From e301c838de4e25f3c885b17f4f6b9211b79b20a0 Mon Sep 17 00:00:00 2001 From: Simon From Jakobsen Date: Thu, 5 Mar 2026 16:04:37 +0100 Subject: [PATCH] init --- .gitignore | 1 + deno.jsonc | 5 + program.ethlang | 9 + src/ast.ts | 105 +++++++++++ src/front.ts | 478 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 34 ++++ src/middle.ts | 13 ++ src/root_syms.ts | 23 +++ src/ty.ts | 43 +++++ 9 files changed, 711 insertions(+) create mode 100644 .gitignore create mode 100644 deno.jsonc create mode 100644 program.ethlang create mode 100644 src/ast.ts create mode 100644 src/front.ts create mode 100644 src/main.ts create mode 100644 src/middle.ts create mode 100644 src/root_syms.ts create mode 100644 src/ty.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..7ebf61b --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,5 @@ +{ + "fmt": { + "indentWidth": 4 + } +} \ No newline at end of file diff --git a/program.ethlang b/program.ethlang new file mode 100644 index 0000000..726f906 --- /dev/null +++ b/program.ethlang @@ -0,0 +1,9 @@ + +fn add(a: int, b: int) -> int { + return __add(a, b); +} + +fn main() -> int { + let sum = add(2, 3); + print_int(sum); +} diff --git a/src/ast.ts b/src/ast.ts new file mode 100644 index 0000000..5a2e946 --- /dev/null +++ b/src/ast.ts @@ -0,0 +1,105 @@ +export class Node { + private static idCounter = 0; + + static create( + line: number, + tag: Tag, + kind: Omit, + ): Node { + return new Node( + Node.idCounter++, + line, + { tag, ...kind } as NodeKind & { tag: Tag }, + ); + } + + private constructor( + public id: number, + public line: number, + public kind: NodeKind, + ) {} + + visit(v: Visitor) { + if (v.visit(this) === "break") { + return; + } + this.visitBelow(v); + } + + visitBelow(v: Visitor) { + const visit = (...nodes: (Node | null)[]) => { + for (const node of nodes) { + node?.visit(v); + } + }; + + const k = this.kind; + switch (k.tag) { + case "Error": + return visit(); + case "File": + return visit(...k.stmts); + case "Block": + return visit(...k.stmts); + case "ExprStmt": + return visit(k.expr); + case "AssignStmt": + return visit(k.place, k.expr); + case "FnStmt": + return visit(...k.params, k.retTy, k.body); + case "ReturnStmt": + return visit(k.expr); + case "LetStmt": + return visit(k.param, k.expr); + case "Param": + return visit(k.ty); + case "IdentExpr": + return visit(); + case "IntExpr": + return visit(); + case "CallExpr": + return visit(k.expr, ...k.args); + case "IdentTy": + return visit(); + } + const _: never = k; + } +} + +export type NodeKind = + | { tag: "Error" } + | { tag: "File"; stmts: Node[] } + | { tag: "Block"; stmts: Node[] } + | { tag: "ExprStmt"; expr: Node } + | { tag: "AssignStmt"; place: Node; expr: Node } + | { + tag: "FnStmt"; + ident: string; + params: Node[]; + retTy: Node | null; + body: Node; + } + | { tag: "ReturnStmt"; expr: Node | null } + | { tag: "LetStmt"; param: Node; expr: Node } + | { tag: "Param"; ident: string; ty: Node | null } + | { tag: "IdentExpr"; ident: string } + | { tag: "IntExpr"; value: number } + | { tag: "CallExpr"; expr: Node; args: Node[] } + | { tag: "IdentTy"; ident: string }; + +export interface Visitor { + visit(node: Node): void | "break"; +} + +export type NodeWithKind< + Tag extends NodeKind["tag"], +> = Node & { kind: { tag: Tag } }; + +export function assertNodeWithKind( + node: Node, + tag: Tag, +): asserts node is NodeWithKind { + if (node.kind.tag !== tag) { + throw new Error(); + } +} diff --git a/src/front.ts b/src/front.ts new file mode 100644 index 0000000..be275c3 --- /dev/null +++ b/src/front.ts @@ -0,0 +1,478 @@ +import * as ast from "./ast.ts"; +import { rootSyms } from "./root_syms.ts"; +import { Ty } from "./ty.ts"; + +export class Checker { + private nodeTys = new Map(); + + constructor( + private filename: string, + private text: string, + private file: ast.Node, + private resols: ResolveMap, + ) {} + + check(node: ast.Node): Ty { + if (this.nodeTys.has(node.id)) { + return this.nodeTys.get(node.id)!; + } + const ty = this.checkNode(node); + this.nodeTys.set(node.id, ty); + return ty; + } + + private checkNode(node: ast.Node): Ty { + const k = node.kind; + + if (k.tag === "FnStmt") { + } + + throw new Error("not checked"); + } + + private error(line: number, message: string): never { + printDiagnostics( + this.filename, + line, + "error", + message, + this.text, + ); + Deno.exit(1); + } +} + +export type Sym = + | { tag: "Error" } + | { tag: "Builtin"; id: string } + | { tag: "Fn"; stmt: ast.NodeWithKind<"FnStmt"> } + | { + tag: "FnParam"; + stmt: ast.NodeWithKind<"FnStmt">; + param: ast.NodeWithKind<"Param">; + } + | { + tag: "Let"; + stmt: ast.NodeWithKind<"LetStmt">; + param: ast.NodeWithKind<"Param">; + }; + +export class ResolveMap { + constructor( + private resols: Map, + ) {} + + get(node: ast.Node): Sym { + if (!this.resols.has(node.id)) { + throw new Error("not resolved"); + } + return this.resols.get(node.id)!; + } +} + +class ResolverSyms { + static root(): ResolverSyms { + return new ResolverSyms( + new Map( + rootSyms.map<[string, Sym]>((sym) => [ + sym.id, + { tag: "Builtin", id: sym.id }, + ]), + ), + null, + ); + } + static forkFrom(parent: ResolverSyms): ResolverSyms { + return new ResolverSyms( + new Map(), + parent, + ); + } + + private constructor( + private syms = new Map(), + public parent: ResolverSyms | null, + ) {} + + define(ident: string, sym: Sym) { + this.syms.set(ident, sym); + } + + resolve(ident: string): Sym | null { + if (this.syms.has(ident)) { + return this.syms.get(ident)!; + } + if (this.parent) { + return this.parent.resolve(ident); + } + return null; + } +} + +export function resolve( + filename: string, + text: string, + file: ast.Node, +): ResolveMap { + let syms = ResolverSyms.root(); + const resols = new Map(); + + file.visit({ + visit(node) { + const k = node.kind; + + if (k.tag === "File" || k.tag === "Block") { + syms = ResolverSyms.forkFrom(syms); + for (const stmt of k.stmts) { + const k = stmt.kind; + if (k.tag === "FnStmt") { + ast.assertNodeWithKind(stmt, "FnStmt"); + syms.define(k.ident, { tag: "Fn", stmt }); + } + } + node.visitBelow(this); + syms = syms.parent!; + return "break"; + } + + if (k.tag === "FnStmt") { + ast.assertNodeWithKind(node, "FnStmt"); + syms = ResolverSyms.forkFrom(syms); + for (const param of k.params) { + ast.assertNodeWithKind(param, "Param"); + syms.define(param.kind.ident, { + tag: "FnParam", + stmt: node, + param, + }); + } + node.visitBelow(this); + syms = syms.parent!; + return "break"; + } + + if (k.tag === "LetStmt") { + const stmt = node as ast.NodeWithKind<"LetStmt">; + const param = k.param as ast.NodeWithKind<"Param">; + syms.define(param.kind.ident, { tag: "Let", stmt, param }); + } + + if (k.tag === "IdentExpr") { + const sym = syms.resolve(k.ident); + if (sym === null) { + printDiagnostics( + filename, + node.line, + "error", + `undefined symbol '${k.ident}'`, + text, + ); + Deno.exit(1); + } + resols.set(node.id, sym); + } + }, + }); + + return new ResolveMap(resols); +} + +export function parse( + filename: string, + text: string, +): ast.Node { + return new Parser(filename, text).parseFile(); +} + +export class Parser { + private toks: Tok[]; + private idx = 0; + private currentLine = 1; + + constructor( + private filename: string, + private text: string, + ) { + this.toks = tokenize(text); + } + + parseFile(): ast.Node { + const loc = this.loc(); + const stmts: ast.Node[] = []; + while (!this.done) { + stmts.push(this.parseStmt()); + } + return ast.Node.create(loc, "File", { stmts }); + } + + parseBlock(): ast.Node { + const loc = this.loc(); + this.mustEat("{"); + const stmts: ast.Node[] = []; + while (!this.done && !this.test("}")) { + stmts.push(this.parseStmt()); + } + this.mustEat("}"); + return ast.Node.create(loc, "Block", { stmts }); + } + + parseStmt(): ast.Node { + const loc = this.loc(); + if (this.test("fn")) { + return this.parseFnStmt(); + } else if (this.test("return")) { + return this.parseReturnStmt(); + } else if (this.test("let")) { + return this.parseLetStmt(); + } else { + const place = this.parseExpr(); + if (this.eat("=")) { + const expr = this.parseExpr(); + this.mustEat(";"); + return ast.Node.create(loc, "AssignStmt", { place, expr }); + } + this.mustEat(";"); + return ast.Node.create(loc, "ExprStmt", { expr: place }); + } + } + + parseFnStmt(): ast.Node { + const loc = this.loc(); + this.step(); + const ident = this.mustEat("ident").value; + this.mustEat("("); + const params: ast.Node[] = []; + if (!this.test(")")) { + params.push(this.parseParam()); + while (this.eat(",")) { + if (this.test(")")) { + break; + } + params.push(this.parseParam()); + } + } + this.mustEat(")"); + let retTy: ast.Node | null = null; + if (this.eat("->")) { + retTy = this.parseTy(); + } + const body = this.parseBlock(); + return ast.Node.create(loc, "FnStmt", { ident, params, retTy, body }); + } + + parseReturnStmt(): ast.Node { + const loc = this.loc(); + this.step(); + let expr: ast.Node | null = null; + if (!this.test(";")) { + expr = this.parseExpr(); + } + this.mustEat(";"); + return ast.Node.create(loc, "ReturnStmt", { expr }); + } + + parseLetStmt(): ast.Node { + const loc = this.loc(); + this.step(); + const param = this.parseParam(); + this.mustEat("="); + const expr = this.parseExpr(); + this.mustEat(";"); + return ast.Node.create(loc, "LetStmt", { param, expr }); + } + + parseParam(): ast.Node { + const loc = this.loc(); + const ident = this.mustEat("ident").value; + let ty: ast.Node | null = null; + if (this.eat(":")) { + ty = this.parseTy(); + } + return ast.Node.create(loc, "Param", { ident, ty }); + } + + parseExpr(): ast.Node { + return this.parsePostfix(); + } + + parsePostfix(): ast.Node { + let expr = this.parseOperand(); + while (true) { + const loc = this.loc(); + if (this.eat("(")) { + const args: ast.Node[] = []; + if (!this.test(")")) { + args.push(this.parseExpr()); + while (this.eat(",")) { + if (this.done || this.test(")")) { + break; + } + args.push(this.parseExpr()); + } + } + this.mustEat(")"); + expr = ast.Node.create(loc, "CallExpr", { expr, args }); + } else { + break; + } + } + return expr; + } + + parseOperand(): ast.Node { + const loc = this.loc(); + if (this.test("ident")) { + const ident = this.current.value; + this.step(); + return ast.Node.create(loc, "IdentExpr", { ident }); + } else if (this.test("int")) { + const value = Number(this.current.value); + this.step(); + return ast.Node.create(loc, "IntExpr", { value }); + } else { + this.mustEat(""); + throw new Error(); + } + } + + parseTy(): ast.Node { + const loc = this.loc(); + if (this.test("ident")) { + const ident = this.current.value; + this.step(); + return ast.Node.create(loc, "IdentTy", { ident }); + } else { + this.mustEat(""); + throw new Error(); + } + } + + private mustEat(type: string, loc: number = this.loc()): Tok { + const tok = this.current; + if (tok.type !== type) { + this.error( + `expected '${type}', got '${ + this.done ? "eof" : this.current.type + }'`, + loc, + ); + } + this.step(); + return tok; + } + + private error(message: string, loc: number): never { + printDiagnostics(this.filename, loc, "error", message, this.text); + Deno.exit(1); + } + + private eat(type: string): boolean { + if (this.test(type)) { + this.step(); + return true; + } + return false; + } + + private step() { + this.idx += 1; + if (!this.done) { + this.currentLine = this.current.line; + } + } + + private test(type: string): boolean { + return !this.done && this.current.type == type; + } + + private loc(): number { + return this.currentLine; + } + + private get current(): Tok { + return this.toks[this.idx]; + } + + private get done(): boolean { + return this.idx >= this.toks.length; + } +} + +export type Tok = { type: string; value: string; line: number }; + +const keywordRegex = + /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)$/; + +export function tokenize(text: string): Tok[] { + return text + .replace(/\/\/[^\n]*/g, "") + .replace(/((?:\-\>)|[\n\(\)\{\},.;:])/g, " $1 ") + .split(/[ \t\r]/) + .filter((value) => value !== "") + .reduce<[[string, number][], number]>( + ([toks, line], value) => { + if (value === "\n") { + return [toks, line + 1]; + } else { + return [[...toks, [value, line]], line]; + } + }, + [[], 1], + )[0] + .map(([value, line]) => ({ type: value, value, line })) + .map((tok) => + /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tok.value) + ? { + ...tok, + type: keywordRegex.test(tok.value) ? tok.value : "ident", + } + : tok + ) + .map((tok) => + /^0|(?:[1-9][0-9]*)$/.test(tok.value) + ? { ...tok, type: "int" } + : tok + ); +} + +export function printDiagnostics( + filename: string, + line: number, + severity: "error", + message: string, + text?: string, +) { + const severityColor = ({ + "error": "red", + } as { [Key in typeof severity]: string })[severity]; + + console.error( + `%c${severity}%c: ${message}\n %c--> ${filename}:${line}%c`, + `color: ${severityColor}; font-weight: bold;`, + "color: white; font-weight: bold;", + "color: gray;", + "", + ); + if (!text) { + return; + } + + const newlines = text + .split("") + .map((ch, idx) => ch === "\n" ? idx : null) + .filter((v) => v !== null); + const lineText = text.slice(newlines[line - 2] + 1, newlines[line - 1]); + const lineNumberText = line.toString(); + + console.error( + `${" ".repeat(lineNumberText.length)}%c|\n` + + `${lineNumberText}|%c${lineText}\n` + + `${" ".repeat(lineNumberText.length)}%c|` + + `%c${"~".repeat(lineText.length)}%c`, + "color: cyan;", + "color: white;", + "color: cyan;", + `color: ${severityColor};`, + "", + ); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..6648e29 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,34 @@ +import * as front from "./front.ts"; +import * as ast from "./ast.ts"; + +const filename = Deno.args[0]; +const text = await Deno.readTextFile(filename); + +const fileAst = front.parse(filename, text); +const resols = front.resolve(filename, text, fileAst); +const checker = new front.Checker(filename, text, fileAst, resols); + +let mainFn: ast.NodeWithKind<"FnStmt"> | null = null; + +fileAst.visit({ + visit(node) { + if (node.kind.tag === "FnStmt" && node.kind.ident === "main") { + if (mainFn) { + console.error("error: multiple 'main' functions"); + Deno.exit(1); + } + + ast.assertNodeWithKind(node, "FnStmt"); + mainFn = node; + } + }, +}); + +if (!mainFn) { + console.error("error: no 'main' function"); + Deno.exit(1); +} + +const mainTy = checker.check(mainFn); + +console.log({ ast: fileAst, resols }); diff --git a/src/middle.ts b/src/middle.ts new file mode 100644 index 0000000..d6d9ae0 --- /dev/null +++ b/src/middle.ts @@ -0,0 +1,13 @@ +export class BasicBlock { + constructor( + public instructions: Inst[], + ) {} +} + +export class Inst { + constructor() {} +} + +export type InsKind = + | { tag: "Error" } + | { tag: "Call" }; diff --git a/src/root_syms.ts b/src/root_syms.ts new file mode 100644 index 0000000..a497a32 --- /dev/null +++ b/src/root_syms.ts @@ -0,0 +1,23 @@ +import { Ty } from "./ty.ts"; + +export type RootSym = { + id: string; + ty: Ty; +}; + +export const rootSyms: RootSym[] = [ + { + id: "print_int", + ty: Ty.create("Fn", { + params: [], + retTy: Ty.Void, + }), + }, + { + id: "__add", + ty: Ty.create("Fn", { + params: [Ty.Int, Ty.Int], + retTy: Ty.Int, + }), + }, +]; diff --git a/src/ty.ts b/src/ty.ts new file mode 100644 index 0000000..f2b474f --- /dev/null +++ b/src/ty.ts @@ -0,0 +1,43 @@ +import * as ast from "./ast.ts"; + +export class Ty { + private static idCounter = 0; + private static internedTys = new Map(); + + static create( + tag: Tag, + kind: Omit, + ): Ty { + const ty = new Ty( + this.idCounter, + { tag, ...kind } as TyKind & { tag: Tag }, + ); + const hash = ty.internHash(); + if (this.internedTys.has(hash)) { + return this.internedTys.get(hash)!; + } + this.internedTys.set(hash, ty); + this.idCounter += 1; + return ty; + } + + static Error = Ty.create("Error", {}); + static Void = Ty.create("Void", {}); + static Int = Ty.create("Int", {}); + + private internHash(): string { + return JSON.stringify(this.kind); + } + + private constructor( + public id: number, + public kind: TyKind, + ) {} +} + +export type TyKind = + | { tag: "Error" } + | { tag: "Void" } + | { tag: "Int" } + | { tag: "Fn"; params: Ty[]; retTy: Ty } + | { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> };