diff --git a/src/diagnostics.ts b/src/diagnostics.ts new file mode 100644 index 0000000..4c5bf22 --- /dev/null +++ b/src/diagnostics.ts @@ -0,0 +1,42 @@ +export function printDiagnostics( + filename: string, + line: number, + severity: "error" | "info", + message: string, + text?: string, +) { + const severityColor = ({ + "error": "red", + "info": "blue", + } as { [Key in typeof severity]: string })[severity]; + + console.error( + `%c${severity}%c: ${message}\n %c--> ${filename}:${line}%c`, + `color: ${severityColor}; font-weight: bold;`, + "color: lightwhite; 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: lightwhite;", + "color: cyan;", + `color: ${severityColor};`, + "", + ); +} diff --git a/src/front.ts b/src/front.ts deleted file mode 100644 index cef61b3..0000000 --- a/src/front.ts +++ /dev/null @@ -1,756 +0,0 @@ -import * as ast from "./ast.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 (node.is("FnStmt")) { - return this.checkFnStmt(node); - } - - if (node.is("Param")) { - const sym = this.resols.get(node); - - if (sym.tag === "Let") { - const exprTy = this.check(sym.stmt.kind.expr); - if (node.kind.ty) { - const explicitTy = this.check(node.kind.ty); - this.assertCompatible( - exprTy, - explicitTy, - sym.stmt.kind.expr.line, - ); - } - return exprTy; - } - if (sym.tag === "FnParam") { - if (!node.kind.ty) { - this.error(node.line, `parameter must have a type`); - this.fail(); - } - return this.check(node.kind.ty); - } - - throw new Error(`'${sym.tag}' not handled`); - } - - if (node.is("IdentExpr")) { - const sym = this.resols.get(node); - if (sym.tag === "Fn") { - return this.check(sym.stmt); - } - if (sym.tag === "Builtin") { - return builtins.find((s) => s.id === sym.id)!.ty; - } - if (sym.tag === "FnParam") { - return this.check(sym.param); - } - if (sym.tag === "Let") { - return this.check(sym.param); - } - throw new Error(`'${sym.tag}' not handled`); - } - - if (node.is("IntExpr")) { - return Ty.Int; - } - - if (node.is("CallExpr")) { - return this.checkCall(node); - } - - if (node.is("BinaryExpr")) { - const left = this.check(node.kind.left); - const right = this.check(node.kind.right); - const binaryOp = binaryOpPatterns - .find((pat) => - pat.op === node.kind.op && - left.compatibleWith(pat.left) && - right.compatibleWith(pat.right) - ); - if (!binaryOp) { - this.error( - node.line, - `operator '${node.kind.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`, - ); - this.fail(); - } - return binaryOp.result; - } - - if (node.is("IdentTy")) { - switch (node.kind.ident) { - case "void": - return Ty.Void; - case "int": - return Ty.Int; - default: - this.error(node.line, `unknown type '${node.kind.ident}'`); - } - } - - throw new Error(`'${k.tag}' not unhandled`); - } - - private checkFnStmt(stmt: ast.NodeWithKind<"FnStmt">): Ty { - const k = stmt.kind; - - const params = k.params.map((param) => this.check(param)); - const retTy = k.retTy ? this.check(k.retTy) : Ty.Void; - - k.body.visit({ - visit: (node) => { - if (node.is("ReturnStmt")) { - const ty = node.kind.expr - ? this.check(node.kind.expr) - : Ty.Void; - if (!ty.compatibleWith(retTy)) { - this.error( - node.line, - `type '${ty.pretty()}' not compatible with return type '${retTy.pretty()}'`, - ); - this.info( - stmt.kind.retTy?.line ?? stmt.line, - `return type '${retTy}' defined here`, - ); - this.fail(); - } - } - }, - }); - - const ty = Ty.create("Fn", { params, retTy }); - return Ty.create("FnStmt", { stmt, ty }); - } - - private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty { - const calleeTy = this.check(node.kind.expr); - - const callableTy = calleeTy.isKind("Fn") - ? calleeTy - : calleeTy.isKind("FnStmt") - ? calleeTy.kind.ty as Ty & { kind: { tag: "Fn" } } - : null; - - if (!callableTy) { - this.error( - node.line, - `type '${calleeTy.pretty()}' not callable`, - ); - this.fail(); - } - - const args = node.kind.args - .map((arg) => this.check(arg)); - const params = callableTy.kind.params; - if (args.length !== params.length) { - this.error( - node.line, - `incorrect amount of arguments. got ${args.length} expected ${params.length}`, - ); - if (calleeTy.isKind("FnStmt")) { - this.info( - calleeTy.kind.stmt.line, - "function defined here", - ); - } - this.fail(); - } - for (const i of args.keys()) { - if (!args[i].compatibleWith(params[i])) { - this.error( - node.kind.args[i].line, - `type '${args[i].pretty()}' not compatible with type '${ - params[i] - }', for argument ${i}`, - ); - if (calleeTy.isKind("FnStmt")) { - this.info( - calleeTy.kind.stmt.kind.params[i].line, - `parameter '${ - calleeTy.kind.stmt.kind.params[i] - .as("Param").kind.ident - }' defined here`, - ); - } - this.fail(); - } - } - return callableTy.kind.retTy; - } - - private assertCompatible(left: Ty, right: Ty, line: number): void { - if (!left.compatibleWith(right)) { - this.error( - line, - `type '${left.pretty()}' not compatible with type '${right.pretty()}'`, - ); - this.fail(); - } - } - - private error(line: number, message: string) { - printDiagnostics( - this.filename, - line, - "error", - message, - this.text, - ); - } - - private info(line: number, message: string) { - printDiagnostics( - this.filename, - line, - "info", - message, - this.text, - ); - } - - private fail(): never { - Deno.exit(1); - } -} - -type BinaryOpPattern = { - op: ast.BinaryOp; - left: Ty; - right: Ty; - result: Ty; -}; - -const binaryOpPatterns: BinaryOpPattern[] = [ - { op: "Add", left: Ty.Int, right: Ty.Int, result: Ty.Int }, - { op: "Subtract", left: Ty.Int, right: Ty.Int, result: Ty.Int }, -]; - -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">; - idx: number; - } - | { - 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(`'${node.kind.tag}' not resolved`); - } - return this.resols.get(node.id)!; - } -} - -class ResolverSyms { - static root(): ResolverSyms { - return new ResolverSyms( - new Map( - builtins.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) { - if (stmt.is("FnStmt")) { - syms.define(stmt.kind.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 [idx, param] of k.params.entries()) { - ast.assertNodeWithKind(param, "Param"); - const sym: Sym = { tag: "FnParam", stmt: node, param, idx }; - syms.define(param.kind.ident, sym); - resols.set(param.id, sym); - } - 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">; - const sym: Sym = { tag: "Let", stmt, param }; - syms.define(param.kind.ident, sym); - resols.set(param.id, sym); - } - - 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); -} - -type Builtin = { - id: string; - ty: Ty; -}; - -const builtins: Builtin[] = [ - { - id: "print_int", - ty: Ty.create("Fn", { - params: [Ty.Int], - retTy: Ty.Void, - }), - }, - { - id: "__add", - ty: Ty.create("Fn", { - params: [Ty.Int, Ty.Int], - retTy: Ty.Int, - }), - }, -]; - -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.parseBinary(); - } - - parseBinary(prec = 7): ast.Node { - const loc = this.loc(); - if (prec == 0) { - return this.parsePrefixE(); - } - const ops: [Tok["type"], ast.BinaryOp, number][] = [ - ["or", "Or", 9], - ["and", "And", 8], - ["==", "Eq", 7], - ["!=", "Ne", 7], - ["<", "Lt", 7], - [">", "Gt", 7], - ["<=", "Lte", 7], - [">=", "Gte", 7], - ["|", "BitOr", 6], - ["^", "BitXor", 5], - ["&", "BitAnd", 4], - ["<<", "Shl", 3], - [">>", "Shr", 3], - ["+", "Add", 2], - ["-", "Subtract", 2], - ["*", "Multiply", 1], - ["/", "Divide", 1], - ["%", "Remainder", 1], - ]; - - let left = this.parseBinary(prec - 1); - - let should_continue = true; - while (should_continue) { - should_continue = false; - for (const [tok, op, p] of ops) { - if (prec >= p && this.eat(tok)) { - const right = this.parseBinary(prec - 1); - left = ast.Node.create( - loc, - "BinaryExpr", - { op, left, right, tok }, - ); - should_continue = true; - break; - } - } - } - return left; - } - - parsePrefixE() { - 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 keywordPattern = - /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)$/; -const operatorPattern = - /((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g; - -export function tokenize(text: string): Tok[] { - return text - .replace(/\/\/[^\n]*/g, "") - .replace(operatorPattern, " $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: keywordPattern.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" | "info", - message: string, - text?: string, -) { - const severityColor = ({ - "error": "red", - "info": "blue", - } as { [Key in typeof severity]: string })[severity]; - - console.error( - `%c${severity}%c: ${message}\n %c--> ${filename}:${line}%c`, - `color: ${severityColor}; font-weight: bold;`, - "color: lightwhite; 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: lightwhite;", - "color: cyan;", - `color: ${severityColor};`, - "", - ); -} diff --git a/src/front/builtins.ts b/src/front/builtins.ts new file mode 100644 index 0000000..0fc99c7 --- /dev/null +++ b/src/front/builtins.ts @@ -0,0 +1,23 @@ +import { Ty } from "../ty.ts"; + +export type Builtin = { + id: string; + ty: Ty; +}; + +export const builtins: Builtin[] = [ + { + id: "print_int", + ty: Ty.create("Fn", { + params: [Ty.Int], + retTy: Ty.Void, + }), + }, + { + id: "__add", + ty: Ty.create("Fn", { + params: [Ty.Int, Ty.Int], + retTy: Ty.Int, + }), + }, +]; diff --git a/src/front/check.ts b/src/front/check.ts new file mode 100644 index 0000000..e3a743d --- /dev/null +++ b/src/front/check.ts @@ -0,0 +1,249 @@ +import * as ast from "../ast.ts"; +import { printDiagnostics } from "../diagnostics.ts"; +import { Ty } from "../ty.ts"; +import { builtins } from "./builtins.ts"; +import { ResolveMap } from "./resolve.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 (node.is("FnStmt")) { + return this.checkFnStmt(node); + } + + if (node.is("Param")) { + const sym = this.resols.get(node); + + if (sym.tag === "Let") { + const exprTy = this.check(sym.stmt.kind.expr); + if (node.kind.ty) { + const explicitTy = this.check(node.kind.ty); + this.assertCompatible( + exprTy, + explicitTy, + sym.stmt.kind.expr.line, + ); + } + return exprTy; + } + if (sym.tag === "FnParam") { + if (!node.kind.ty) { + this.error(node.line, `parameter must have a type`); + this.fail(); + } + return this.check(node.kind.ty); + } + + throw new Error(`'${sym.tag}' not handled`); + } + + if (node.is("IdentExpr")) { + const sym = this.resols.get(node); + if (sym.tag === "Fn") { + return this.check(sym.stmt); + } + if (sym.tag === "Builtin") { + return builtins.find((s) => s.id === sym.id)!.ty; + } + if (sym.tag === "FnParam") { + return this.check(sym.param); + } + if (sym.tag === "Let") { + return this.check(sym.param); + } + throw new Error(`'${sym.tag}' not handled`); + } + + if (node.is("IntExpr")) { + return Ty.Int; + } + + if (node.is("CallExpr")) { + return this.checkCall(node); + } + + if (node.is("BinaryExpr")) { + const left = this.check(node.kind.left); + const right = this.check(node.kind.right); + const binaryOp = binaryOpPatterns + .find((pat) => + pat.op === node.kind.op && + left.compatibleWith(pat.left) && + right.compatibleWith(pat.right) + ); + if (!binaryOp) { + this.error( + node.line, + `operator '${node.kind.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`, + ); + this.fail(); + } + return binaryOp.result; + } + + if (node.is("IdentTy")) { + switch (node.kind.ident) { + case "void": + return Ty.Void; + case "int": + return Ty.Int; + default: + this.error(node.line, `unknown type '${node.kind.ident}'`); + } + } + + throw new Error(`'${k.tag}' not unhandled`); + } + + private checkFnStmt(stmt: ast.NodeWithKind<"FnStmt">): Ty { + const k = stmt.kind; + + const params = k.params.map((param) => this.check(param)); + const retTy = k.retTy ? this.check(k.retTy) : Ty.Void; + + k.body.visit({ + visit: (node) => { + if (node.is("ReturnStmt")) { + const ty = node.kind.expr + ? this.check(node.kind.expr) + : Ty.Void; + if (!ty.compatibleWith(retTy)) { + this.error( + node.line, + `type '${ty.pretty()}' not compatible with return type '${retTy.pretty()}'`, + ); + this.info( + stmt.kind.retTy?.line ?? stmt.line, + `return type '${retTy}' defined here`, + ); + this.fail(); + } + } + }, + }); + + const ty = Ty.create("Fn", { params, retTy }); + return Ty.create("FnStmt", { stmt, ty }); + } + + private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty { + const calleeTy = this.check(node.kind.expr); + + const callableTy = calleeTy.isKind("Fn") + ? calleeTy + : calleeTy.isKind("FnStmt") + ? calleeTy.kind.ty as Ty & { kind: { tag: "Fn" } } + : null; + + if (!callableTy) { + this.error( + node.line, + `type '${calleeTy.pretty()}' not callable`, + ); + this.fail(); + } + + const args = node.kind.args + .map((arg) => this.check(arg)); + const params = callableTy.kind.params; + if (args.length !== params.length) { + this.error( + node.line, + `incorrect amount of arguments. got ${args.length} expected ${params.length}`, + ); + if (calleeTy.isKind("FnStmt")) { + this.info( + calleeTy.kind.stmt.line, + "function defined here", + ); + } + this.fail(); + } + for (const i of args.keys()) { + if (!args[i].compatibleWith(params[i])) { + this.error( + node.kind.args[i].line, + `type '${args[i].pretty()}' not compatible with type '${ + params[i] + }', for argument ${i}`, + ); + if (calleeTy.isKind("FnStmt")) { + this.info( + calleeTy.kind.stmt.kind.params[i].line, + `parameter '${ + calleeTy.kind.stmt.kind.params[i] + .as("Param").kind.ident + }' defined here`, + ); + } + this.fail(); + } + } + return callableTy.kind.retTy; + } + + private assertCompatible(left: Ty, right: Ty, line: number): void { + if (!left.compatibleWith(right)) { + this.error( + line, + `type '${left.pretty()}' not compatible with type '${right.pretty()}'`, + ); + this.fail(); + } + } + + private error(line: number, message: string) { + printDiagnostics( + this.filename, + line, + "error", + message, + this.text, + ); + } + + private info(line: number, message: string) { + printDiagnostics( + this.filename, + line, + "info", + message, + this.text, + ); + } + + private fail(): never { + Deno.exit(1); + } +} + +type BinaryOpPattern = { + op: ast.BinaryOp; + left: Ty; + right: Ty; + result: Ty; +}; + +const binaryOpPatterns: BinaryOpPattern[] = [ + { op: "Add", left: Ty.Int, right: Ty.Int, result: Ty.Int }, + { op: "Subtract", left: Ty.Int, right: Ty.Int, result: Ty.Int }, +]; diff --git a/src/front/mod.ts b/src/front/mod.ts new file mode 100644 index 0000000..84a7d34 --- /dev/null +++ b/src/front/mod.ts @@ -0,0 +1,3 @@ +export * from "./parse.ts"; +export * from "./resolve.ts"; +export * from "./check.ts"; diff --git a/src/front/parse.ts b/src/front/parse.ts new file mode 100644 index 0000000..6c2f275 --- /dev/null +++ b/src/front/parse.ts @@ -0,0 +1,313 @@ +import * as ast from "../ast.ts"; +import { printDiagnostics } from "../diagnostics.ts"; + +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.parseBinary(); + } + + parseBinary(prec = 7): ast.Node { + const loc = this.loc(); + if (prec == 0) { + return this.parsePrefixE(); + } + const ops: [Tok["type"], ast.BinaryOp, number][] = [ + ["or", "Or", 9], + ["and", "And", 8], + ["==", "Eq", 7], + ["!=", "Ne", 7], + ["<", "Lt", 7], + [">", "Gt", 7], + ["<=", "Lte", 7], + [">=", "Gte", 7], + ["|", "BitOr", 6], + ["^", "BitXor", 5], + ["&", "BitAnd", 4], + ["<<", "Shl", 3], + [">>", "Shr", 3], + ["+", "Add", 2], + ["-", "Subtract", 2], + ["*", "Multiply", 1], + ["/", "Divide", 1], + ["%", "Remainder", 1], + ]; + + let left = this.parseBinary(prec - 1); + + let should_continue = true; + while (should_continue) { + should_continue = false; + for (const [tok, op, p] of ops) { + if (prec >= p && this.eat(tok)) { + const right = this.parseBinary(prec - 1); + left = ast.Node.create( + loc, + "BinaryExpr", + { op, left, right, tok }, + ); + should_continue = true; + break; + } + } + } + return left; + } + + parsePrefixE() { + 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 keywordPattern = + /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)$/; +const operatorPattern = + /((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g; + +export function tokenize(text: string): Tok[] { + return text + .replace(/\/\/[^\n]*/g, "") + .replace(operatorPattern, " $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: keywordPattern.test(tok.value) ? tok.value : "ident", + } + : tok + ) + .map((tok) => + /^0|(?:[1-9][0-9]*)$/.test(tok.value) + ? { ...tok, type: "int" } + : tok + ); +} diff --git a/src/front/resolve.ts b/src/front/resolve.ts new file mode 100644 index 0000000..b81ea80 --- /dev/null +++ b/src/front/resolve.ts @@ -0,0 +1,138 @@ +import * as ast from "../ast.ts"; +import { printDiagnostics } from "../diagnostics.ts"; +import { Ty } from "../ty.ts"; +import { builtins } from "./builtins.ts"; + +export class ResolveMap { + constructor( + private resols: Map, + ) {} + + get(node: ast.Node): Sym { + if (!this.resols.has(node.id)) { + throw new Error(`'${node.kind.tag}' not resolved`); + } + return this.resols.get(node.id)!; + } +} + +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">; + idx: number; + } + | { + tag: "Let"; + stmt: ast.NodeWithKind<"LetStmt">; + param: ast.NodeWithKind<"Param">; + }; + +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) { + if (stmt.is("FnStmt")) { + syms.define(stmt.kind.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 [idx, param] of k.params.entries()) { + ast.assertNodeWithKind(param, "Param"); + const sym: Sym = { tag: "FnParam", stmt: node, param, idx }; + syms.define(param.kind.ident, sym); + resols.set(param.id, sym); + } + 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">; + const sym: Sym = { tag: "Let", stmt, param }; + syms.define(param.kind.ident, sym); + resols.set(param.id, sym); + } + + 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); +} + +class ResolverSyms { + static root(): ResolverSyms { + return new ResolverSyms( + new Map( + builtins.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; + } +} diff --git a/src/main.ts b/src/main.ts index 9974db1..0c8399a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import * as ast from "./ast.ts"; -import * as front from "./front.ts"; +import * as front from "./front/mod.ts"; import * as middle from "./middle.ts"; import { MirInterpreter } from "./mir_interpreter.ts"; diff --git a/src/middle.ts b/src/middle.ts index 186b83e..c15ebd3 100644 --- a/src/middle.ts +++ b/src/middle.ts @@ -1,5 +1,5 @@ import * as ast from "./ast.ts"; -import { Checker, ResolveMap } from "./front.ts"; +import { Checker, ResolveMap } from "./front/mod.ts"; import { Ty } from "./ty.ts"; export class MiddleLowerer {