diff --git a/compiler/ast/ast.ts b/compiler/ast/ast.ts new file mode 100644 index 0000000..4040f6e --- /dev/null +++ b/compiler/ast/ast.ts @@ -0,0 +1,23 @@ +export type File = { + stmts: Stmt[]; +}; + +export type Stmt = { + id: number; + kind: StmtKind; +}; + +export type StmtKind = + | { tag: "error" } + | { tag: "mod_block" } & ModBlockStmt + | { tag: "mod_file" } & ModFileStmt; + +export type ModBlockStmt = { + ident: string; + stmts: Stmt[]; +}; + +export type ModFileStmt = { + ident: string; + filePath: string; +}; diff --git a/compiler/ast/mod.ts b/compiler/ast/mod.ts new file mode 100644 index 0000000..7627d46 --- /dev/null +++ b/compiler/ast/mod.ts @@ -0,0 +1,2 @@ +export * from "./ast.ts"; +export * from "./visitor.ts"; diff --git a/compiler/ast/visitor.ts b/compiler/ast/visitor.ts new file mode 100644 index 0000000..7c4ed88 --- /dev/null +++ b/compiler/ast/visitor.ts @@ -0,0 +1,62 @@ +import { exhausted } from "../util.ts"; +import { File, ModBlockStmt, ModFileStmt, Stmt } from "./ast.ts"; + +export type VisitRes = "stop" | void; + +type R = VisitRes; +type PM = unknown[]; + +export interface Visitor< + P extends PM = [], +> { + visitFile?(file: File, ...p: P): R; + visitStmt?(stmt: Stmt, ...p: P): R; + visitErrorStmt?(stmt: Stmt, ...p: P): R; + visitModBlockStmt?(stmt: Stmt, kind: ModBlockStmt, ...p: P): R; + visitModFileStmt?(stmt: Stmt, kind: ModFileStmt, ...p: P): R; +} + +export function visitFile< + P extends PM = [], +>( + v: Visitor

, + file: File, + ...p: P +) { + if (v.visitFile?.(file, ...p) === "stop") return; + visitStmts(v, file.stmts, ...p); +} + +export function visitStmts< + P extends PM = [], +>( + v: Visitor

, + stmts: Stmt[], + ...p: P +) { + for (const stmt of stmts) { + visitStmt(v, stmt, ...p); + } +} + +export function visitStmt< + P extends PM = [], +>( + v: Visitor

, + stmt: Stmt, + ...p: P +) { + switch (stmt.kind.tag) { + case "error": + if (v.visitErrorStmt?.(stmt, ...p) === "stop") return; + return; + case "mod_block": + if (v.visitModBlockStmt?.(stmt, stmt.kind, ...p) === "stop") return; + visitStmts(v, stmt.kind.stmts, ...p); + return; + case "mod_file": + if (v.visitModFileStmt?.(stmt, stmt.kind, ...p) === "stop") return; + return; + } + exhausted(stmt.kind); +} diff --git a/compiler/ctx.ts b/compiler/ctx.ts new file mode 100644 index 0000000..5d0267b --- /dev/null +++ b/compiler/ctx.ts @@ -0,0 +1,107 @@ +import * as ast from "./ast/mod.ts"; +import { Diag } from "./diagnostics.ts"; + +export class Ctx { + private fileIds = new Ids(); + private files = new Map, FileInfo>(); + + private diags: Diag[] = []; + + public fileHasChildWithIdent(file: File, childIdent: string): boolean { + return this.files.get(id(file))! + .subFiles.has(childIdent); + } + + public addFile( + ident: string, + absPath: string, + relPath: string, + superFile: File | undefined, + text: string, + ): File { + const file = this.fileIds.nextThenStep(); + this.files.set(id(file), { + ident, + absPath, + relPath, + superFile, + subFiles: new Map(), + text, + }); + if (superFile) { + this.files.get(id(superFile))! + .subFiles.set(ident, file); + } + return file; + } + + public addFileAst(file: File, ast: ast.File) { + this.files.get(id(file))!.ast = ast; + } + + public fileInfo(file: File): FileInfo { + this.files.get(id(file))!; + } + + public reportFatal(file: File, msg: string) { + console.error(`fatal: ${msg}`); + this.reportImmediately(file, msg); + } + + public enableReportImmediately = false; + public enableStacktrace = false; + private reportImmediately(file: File, msg: string) { + if (!this.enableReportImmediately) { + return; + } + + if (!this.enableStacktrace) { + return; + } + class StackTracer extends Error { + constructor() { + super("StackTracer"); + } + } + try { + //throw new ReportNotAnError(); + } catch (error) { + if (!(error instanceof StackTracer)) { + throw error; + } + console.log( + error.stack?.replace( + "Error: StackTracer", + "Stack trace:", + ) ?? + error, + ); + } + } +} + +export type File = IdBase; + +export type FileInfo = { + ident: string; + absPath: string; + relPath: string; + superFile?: File; + subFiles: Map; + text: string; + ast?: ast.File; +}; + +export type IdBase = { id: number }; + +export type Id = IdType["id"]; +export const id = (id: IdType): Id => id.id; + +export class Ids { + private next = 0; + public nextThenStep(): IdType { + const id = this.next; + this.next += 1; + return { id } as IdType; + } +} diff --git a/compiler/diagnostics.ts b/compiler/diagnostics.ts new file mode 100644 index 0000000..080c030 --- /dev/null +++ b/compiler/diagnostics.ts @@ -0,0 +1,43 @@ +import { Ctx, File } from "./ctx.ts"; + +export type Span = { + begin: Pos; + end: Pos; +}; + +export type Pos = { + idx: number; + line: number; + col: number; +}; + +export type Diag = { + severity: "fatal" | "error" | "warning" | "info"; + origin?: string; + msg: string; + file?: File; + span?: Span; + pos?: Pos; +}; + +export function prettyPrintDiag(ctx: Ctx, diag: Diag) { + const { severity, msg } = diag; + const origin = diag.origin ? `${diag.origin}: ` : ""; + console.error(`${origin}${severity}: ${msg}`); + if (diag.file && (diag.span || diag.pos)) { + const { absPath: path, text } = ctx.fileInfo(diag.file); + const { line, col } = diag.span?.begin ?? diag.pos!; + console.error(` at ./${path}:${line}:${col}`); + if (diag.span) { + let begin = diag.span.begin.idx; + while (begin >= 0 && text[begin - 1] != "\n") { + begin -= 1; + } + let end = diag.span.end.idx; + while (end < text.length && text[end + 1] != "\n") { + end += 1; + } + } else if (diag.pos) { + } + } +} diff --git a/compiler/main.ts b/compiler/main.ts index c87de99..c0b7153 100644 --- a/compiler/main.ts +++ b/compiler/main.ts @@ -1,14 +1,20 @@ -type Pack = { +import * as path from "jsr:@std/path"; +import { Parser } from "./parser/parser.ts"; +import * as ast from "./ast/mod.ts"; +import { Ctx } from "./ctx.ts"; +import { File } from "./ctx.ts"; + +export type Pack = { rootMod: Mod; }; -type Mod = null; +export type Mod = null; -interface PackEmitter { +export interface PackEmitter { emit(pack: Pack): void; } -class PackCompiler { +export class PackCompiler { public constructor( private entryFilePath: string, private emitter: PackEmitter, @@ -18,5 +24,53 @@ class PackCompiler { } } -class ModResolver { +type _P = { file: File }; +export class FileTreeCollector implements ast.Visitor<[_P]> { + private subFilePromise = Promise.resolve(); + + public constructor( + private ctx: Ctx, + private superFile: File | undefined, + private ident: string, + private absPath: string, + private relPath: string, + ) {} + + public async collect(): Promise { + const text = await Deno.readTextFile(this.absPath); + const file = this.ctx.addFile( + this.ident, + this.absPath, + this.relPath, + this.superFile, + text, + ); + const fileAst = new Parser(file, text).parse(); + this.ctx.addFileAst(file, fileAst); + ast.visitFile(this, fileAst, { file }); + await this.subFilePromise; + } + + visitModFileStmt( + _stmt: ast.Stmt, + kind: ast.ModFileStmt, + { file }: _P, + ): ast.VisitRes { + const { ident, filePath: relPath } = kind; + const absPath = path.join(path.dirname(this.absPath), relPath); + this.subFilePromise = this.subFilePromise + .then(() => { + if (this.ctx.fileHasChildWithIdent(file, ident)) { + } + return new FileTreeCollector( + this.ctx, + file, + ident, + absPath, + relPath, + ) + .collect(); + }); + return "stop"; + } } diff --git a/compiler/parser/parser.ts b/compiler/parser/parser.ts new file mode 100644 index 0000000..15186af --- /dev/null +++ b/compiler/parser/parser.ts @@ -0,0 +1,14 @@ +import { File } from "../ast/ast.ts"; +import { File as CtxFile } from "../ctx.ts"; +import { todo } from "../util.ts"; + +export class Parser { + public constructor( + private file: CtxFile, + private text: string, + ) {} + + public parse(): File { + return todo(); + } +} diff --git a/compiler/util.ts b/compiler/util.ts new file mode 100644 index 0000000..7b8b9bf --- /dev/null +++ b/compiler/util.ts @@ -0,0 +1,9 @@ +export function todo(msg?: string): T { + class NotImplemented extends Error {} + throw new NotImplemented(msg); +} + +export function exhausted(_: never) { + class Unexhausted extends Error {} + throw new Unexhausted(); +}