diff --git a/compiler/Checker.ts b/compiler/Checker.ts new file mode 100644 index 0000000..28b780b --- /dev/null +++ b/compiler/Checker.ts @@ -0,0 +1,229 @@ +import { EType, Expr } from "./ast.ts"; +import { Pos } from "./Token.ts"; +import { VType, VTypeParam, vtypesEqual, vtypeToString } from "./vtypes.ts"; + +export class Checker { + public report(msg: string, pos: Pos) { + console.error(`${msg} at ${pos.line}:${pos.col}`); + } + public checkEType(etype: EType): VType { + const pos = etype.pos; + if (etype.kind.type === "ident") { + if (etype.kind.value === "null") { + return { type: "null" }; + } + if (etype.kind.value === "int") { + return { type: "int" }; + } + if (etype.kind.value === "bool") { + return { type: "bool" }; + } + if (etype.kind.value === "string") { + return { type: "string" }; + } + this.report(`undefined type '${etype.kind.value}'`, pos); + return { type: "error" }; + } + if (etype.kind.type === "array") { + const inner = this.checkEType(etype.kind.inner); + return { type: "array", inner }; + } + if (etype.kind.type === "struct") { + const noTypeTest = etype.kind.fields.reduce( + (acc, param) => [acc[0] || !param.etype, param.ident], + [false, ""], + ); + if (noTypeTest[0]) { + this.report( + `field '${noTypeTest[1]}' declared without type`, + pos, + ); + return { type: "error" }; + } + const declaredTwiceTest = etype.kind.fields.reduce< + [boolean, string[], string] + >( + (acc, curr) => { + if (acc[0]) { + return acc; + } + if (acc[1].includes(curr.ident)) { + return [true, acc[1], curr.ident]; + } + return [false, [...acc[1], curr.ident], ""]; + }, + [false, [], ""], + ); + if ( + declaredTwiceTest[0] + ) { + this.report(`field ${declaredTwiceTest[2]} defined twice`, pos); + return { type: "error" }; + } + const fields = etype.kind.fields.map((param): VTypeParam => ({ + ident: param.ident, + vtype: this.checkEType(param.etype!), + })); + return { type: "struct", fields }; + } + throw new Error(`unknown explicit type ${etype.kind.type}`); + } + + public checkExpr(expr: Expr): VType { + const pos = expr.pos; + const vtype = ((): VType => { + switch (expr.kind.type) { + case "error": + throw new Error("error in AST"); + case "ident": + throw new Error("ident expr in AST"); + case "sym": + return this.checkSymExpr(expr); + case "null": + return { type: "null" }; + case "int": + return { type: "int" }; + case "bool": + return { type: "bool" }; + case "string": + return { type: "string" }; + case "binary": + return this.checkBinaryExpr(expr); + case "group": + return this.checkExpr(expr.kind.expr); + case "field": { + const subject = this.checkExpr(expr.kind.subject); + if (subject.type !== "struct") { + this.report("cannot use field on non-struct", pos); + return { type: "error" }; + } + const value = expr.kind.value; + const found = subject.fields.find((param) => + param.ident === value + ); + if (!found) { + this.report( + `no field named '${expr.kind.value}' on struct`, + pos, + ); + return { type: "error" }; + } + return found.vtype; + } + case "index": { + const subject = this.checkExpr(expr.kind.subject); + if (subject.type !== "array") { + this.report("cannot index on non-array", pos); + return { type: "error" }; + } + return subject.inner; + } + case "call": { + const subject = this.checkExpr(expr.kind.subject); + if (subject.type !== "fn") { + this.report("cannot call non-fn", pos); + return { type: "error" }; + } + const args = expr.kind.args.map((arg) => + this.checkExpr(arg) + ); + if (args.length !== subject.params.length) { + this.report( + `incorrect number of arguments` + + `, expected ${subject.params.length}`, + pos, + ); + } + for (let i = 0; i < args.length; ++i) { + if (!vtypesEqual(args[i], subject.params[i].vtype)) { + this.report( + `incorrect argument ${i} '${ + subject.params[i].ident + }'` + + `, expected ${ + vtypeToString(subject.params[i].vtype) + }` + + `, got ${vtypeToString(args[i])}`, + pos, + ); + break; + } + } + return subject.returnType; + } + case "unary": + case "if": + case "loop": + case "block": + } + throw new Error(`unhandled type ${expr.kind.type}`); + })(); + expr.vtype = vtype; + throw new Error(`unknown expression ${expr.kind.type}`); + } + + public checkSymExpr(expr: Expr): VType { + const pos = expr.pos; + if (expr.kind.type !== "sym") { + throw new Error(); + } + } + + public checkBinaryExpr(expr: Expr): VType { + const pos = expr.pos; + if (expr.kind.type !== "binary") { + throw new Error(); + } + const left = this.checkExpr(expr.kind.left); + const right = this.checkExpr(expr.kind.right); + for (const operation of simpleBinaryOperations) { + if (operation.binaryType !== expr.kind.binaryType) { + continue; + } + if (!vtypesEqual(operation.operand, left)) { + continue; + } + if (!vtypesEqual(left, right)) { + continue; + } + return operation.result ?? operation.operand; + } + this.report( + `cannot apply binary operation '${expr.kind.binaryType}' ` + + `on types '${vtypeToString(left)}' and '${ + vtypeToString(right) + }'`, + pos, + ); + return { type: "error" }; + } +} + +const simpleBinaryOperations: { + binaryType: string; + operand: VType; + result?: VType; +}[] = [ + // arithmetic + { binaryType: "+", operand: { type: "int" } }, + { binaryType: "-", operand: { type: "int" } }, + { binaryType: "*", operand: { type: "int" } }, + { binaryType: "/", operand: { type: "int" } }, + // logical + { binaryType: "and", operand: { type: "bool" } }, + { binaryType: "or", operand: { type: "bool" } }, + // equality + { binaryType: "==", operand: { type: "null" }, result: { type: "bool" } }, + { binaryType: "==", operand: { type: "int" }, result: { type: "bool" } }, + { binaryType: "==", operand: { type: "string" }, result: { type: "bool" } }, + { binaryType: "==", operand: { type: "bool" }, result: { type: "bool" } }, + { binaryType: "!=", operand: { type: "null" }, result: { type: "bool" } }, + { binaryType: "!=", operand: { type: "int" }, result: { type: "bool" } }, + { binaryType: "!=", operand: { type: "string" }, result: { type: "bool" } }, + { binaryType: "!=", operand: { type: "bool" }, result: { type: "bool" } }, + // comparison + { binaryType: "<", operand: { type: "int" }, result: { type: "bool" } }, + { binaryType: ">", operand: { type: "int" }, result: { type: "bool" } }, + { binaryType: "<=", operand: { type: "int" }, result: { type: "bool" } }, + { binaryType: ">=", operand: { type: "int" }, result: { type: "bool" } }, +]; diff --git a/compiler/Lexer.ts b/compiler/Lexer.ts index 1752ee4..f7813dd 100644 --- a/compiler/Lexer.ts +++ b/compiler/Lexer.ts @@ -5,16 +5,17 @@ export class Lexer { private line = 1; private col = 1; - - public constructor (private text: string) {} + public constructor(private text: string) {} public next(): Token | null { - if (this.done()) + if (this.done()) { return null; + } const pos = this.pos(); if (this.test(/[ \t\n\r]/)) { - while (!this.done() && this.test(/[ \t\n\r]/)) + while (!this.done() && this.test(/[ \t\n\r]/)) { this.step(); + } return this.next(); } @@ -39,6 +40,8 @@ export class Lexer { return this.token("if", pos); case "else": return this.token("else", pos); + case "struct": + return this.token("struct", pos); default: return { ...this.token("ident", pos), identValue: value }; } @@ -53,25 +56,26 @@ export class Lexer { } if (this.test("0")) { - this.step() + this.step(); if (!this.done() && this.test(/[0-9]/)) { console.error( - `Lexer: invalid number` - + ` at ${pos.line}:${pos.col}`, + `Lexer: invalid number` + + ` at ${pos.line}:${pos.col}`, ); return this.token("error", pos); } - return { ...this.token("int", pos), intValue: 0}; + return { ...this.token("int", pos), intValue: 0 }; } - if (this.test("\"")) { + if (this.test('"')) { this.step(); let value = ""; - while (!this.done() && !this.test("\"")) { + while (!this.done() && !this.test('"')) { if (this.test("\\")) { this.step(); - if (this.done()) + if (this.done()) { break; + } value += { "n": "\n", "t": "\t", @@ -82,10 +86,10 @@ export class Lexer { } this.step(); } - if (this.done() || !this.test("\"")) { + if (this.done() || !this.test('"')) { console.error( - `Lexer: unclosed/malformed string` - + ` at ${pos.line}:${pos.col}`, + `Lexer: unclosed/malformed string` + + ` at ${pos.line}:${pos.col}`, ); return this.token("error", pos); } @@ -113,7 +117,7 @@ export class Lexer { return this.token("->", pos); } if (this.test("=")) { - this.step() + this.step(); return this.token("-=", pos); } } @@ -125,16 +129,21 @@ export class Lexer { this.step(); return this.token("+=", pos); } + if (first === "-" && !this.done() && this.test(">")) { + this.step(); + return this.token("->", pos); + } return this.token(first, pos); } if (this.test("/")) { - this.step() + this.step(); if (this.test("/")) { - while (!this.done() && !this.test("\n")) + while (!this.done() && !this.test("\n")) { this.step(); - return this.next() + } + return this.next(); } - return this.token("/", pos) + return this.token("/", pos); } if (this.test("false")) { this.step(); @@ -180,20 +189,29 @@ export class Lexer { this.step(); return this.token("return", pos); } - console.error(`Lexer: illegal character '${this.current()}' at ${pos.line}:${pos.col}`); + console.error( + `Lexer: illegal character '${this.current()}' at ${pos.line}:${pos.col}`, + ); this.step(); - return this.next(); + return this.next(); } - - private done(): boolean { return this.index >= this.text.length; } - - private current(): string { return this.text[this.index]; } - public currentPos(): Pos { return this.pos(); } + private done(): boolean { + return this.index >= this.text.length; + } + + private current(): string { + return this.text[this.index]; + } + + public currentPos(): Pos { + return this.pos(); + } private step() { - if (this.done()) + if (this.done()) { return; + } if (this.current() === "\n") { this.line += 1; this.col = 1; @@ -207,8 +225,8 @@ export class Lexer { return { index: this.index, line: this.line, - col: this.col - } + col: this.col, + }; } private token(type: string, pos: Pos): Token { @@ -217,13 +235,10 @@ export class Lexer { } private test(pattern: RegExp | string): boolean { - if (typeof pattern === "string") + if (typeof pattern === "string") { return this.current() === pattern; - else + } else { return pattern.test(this.current()); + } } - - - - } diff --git a/compiler/Parser.ts b/compiler/Parser.ts index 7d50c6b..ddf61e1 100644 --- a/compiler/Parser.ts +++ b/compiler/Parser.ts @@ -1,4 +1,13 @@ -import { Expr, ExprKind, Param, Stmt, StmtKind, BinaryType} from "./ast.ts"; +import { + BinaryType, + EType, + ETypeKind, + Expr, + ExprKind, + Param, + Stmt, + StmtKind, +} from "./ast.ts"; import { Lexer } from "./Lexer.ts"; import { Pos, Token } from "./Token.ts"; @@ -10,12 +19,19 @@ export class Parser { this.currentToken = lexer.next(); } - private step() { this.currentToken = this.lexer.next() } - public done(): boolean { return this.currentToken == null; } - private current(): Token { return this.currentToken!; } + private step() { + this.currentToken = this.lexer.next(); + } + public done(): boolean { + return this.currentToken == null; + } + private current(): Token { + return this.currentToken!; + } private pos(): Pos { - if (this.done()) + if (this.done()) { return this.lexer.currentPos(); + } return this.current().pos; } @@ -25,13 +41,18 @@ export class Parser { private report(msg: string, pos = this.pos()) { console.log(`Parser: ${msg} at ${pos.line}:${pos.col}`); - class ReportNotAnError extends Error { constructor() { super("ReportNotAnError"); } } + class ReportNotAnError extends Error { + constructor() { + super("ReportNotAnError"); + } + } try { throw new ReportNotAnError(); } catch (error) { - if (!(error instanceof ReportNotAnError)) + if (!(error instanceof ReportNotAnError)) { throw error; - console.log(error) + } + console.log(error); } } @@ -47,33 +68,47 @@ export class Parser { return { kind, pos, id }; } + private etype(kind: ETypeKind, pos: Pos): EType { + const id = this.nextNodeId; + this.nextNodeId += 1; + return { kind, pos, id }; + } + private parseMultiLineBlockExpr(): Expr { const pos = this.pos(); - if (this.test("{")) + if (this.test("{")) { return this.parseBlock(); - if (this.test("if")) + } + if (this.test("if")) { return this.parseIf(); - if (this.test("loop")) + } + if (this.test("loop")) { return this.parseLoop(); + } this.report("expected expr"); return this.expr({ type: "error" }, pos); } private parseSingleLineBlockStmt(): Stmt { const pos = this.pos(); - if (this.test("let")) + if (this.test("let")) { return this.parseLet(); - if (this.test("return")) + } + if (this.test("return")) { return this.parseReturn(); - if (this.test("break")) + } + if (this.test("break")) { return this.parseBreak(); + } this.report("expected stmt"); return this.stmt({ type: "error" }, pos); } private eatSemicolon() { if (!this.test(";")) { - this.report(`expected ';', got '${this.currentToken?.type ?? "eof"}'`); + this.report( + `expected ';', got '${this.currentToken?.type ?? "eof"}'`, + ); return; } this.step(); @@ -91,11 +126,12 @@ export class Parser { if (this.test("}")) { this.step(); return this.expr({ type: "block", stmts }, pos); - } else if (this.test("return") || this.test("break") || this.test("let")) { + } else if ( + this.test("return") || this.test("break") || this.test("let") + ) { stmts.push(this.parseSingleLineBlockStmt()); this.eatSemicolon(); - } - else if (this.test("fn")) { + } else if (this.test("fn")) { stmts.push(this.parseSingleLineBlockStmt()); stmts.push(this.parseFn()); } else if (this.test("{") || this.test("if") || this.test("loop")) { @@ -111,7 +147,12 @@ export class Parser { this.step(); const value = this.parseExpr(); this.eatSemicolon(); - stmts.push(this.stmt({ type: "assign", subject: expr, value }, pos)); + stmts.push( + this.stmt( + { type: "assign", subject: expr, value }, + pos, + ), + ); } else if (this.test(";")) { this.step(); stmts.push(this.stmt({ type: "expr", expr }, expr.pos)); @@ -133,13 +174,15 @@ export class Parser { while (!this.done()) { if (this.test("fn")) { stmts.push(this.parseFn()); - } else if (this.test("let") || this.test("return") || this.test("break")) { + } else if ( + this.test("let") || this.test("return") || this.test("break") + ) { stmts.push(this.parseSingleLineBlockStmt()); this.eatSemicolon(); } else if (this.test("{") || this.test("if") || this.test("loop")) { const expr = this.parseMultiLineBlockExpr(); stmts.push(this.stmt({ type: "expr", expr }, expr.pos)); - } else { + } else { stmts.push(this.parseAssign()); this.eatSemicolon(); } @@ -161,12 +204,20 @@ export class Parser { return this.stmt({ type: "error" }, pos); } const params = this.parseFnParams(); + let returnType: EType | null = null; + if (this.test("->")) { + this.step(); + returnType = this.parseEType(); + } if (!this.test("{")) { this.report("expected block"); return this.stmt({ type: "error" }, pos); } const body = this.parseBlock(); - return this.stmt({ type: "fn", ident, params, body }, pos); + if (returnType === null) { + return this.stmt({ type: "fn", ident, params, body }, pos); + } + return this.stmt({ type: "fn", ident, params, returnType, body }, pos); } public parseFnParams(): Param[] { @@ -175,18 +226,21 @@ export class Parser { this.step(); return []; } - let params: Param[] = []; + const params: Param[] = []; const paramResult = this.parseParam(); - if (!paramResult.ok) + if (!paramResult.ok) { return []; + } params.push(paramResult.value); while (this.test(",")) { this.step(); - if (this.test(")")) + if (this.test(")")) { break; + } const paramResult = this.parseParam(); - if (!paramResult.ok) + if (!paramResult.ok) { return []; + } params.push(paramResult.value); } if (!this.test(")")) { @@ -197,11 +251,15 @@ export class Parser { return params; } - public parseParam(): { ok: true, value: Param } | { ok: false } { + public parseParam(): { ok: true; value: Param } | { ok: false } { const pos = this.pos(); if (this.test("ident")) { const ident = this.current().identValue!; this.step(); + if (this.test(":")) { + const etype = this.parseEType(); + return { ok: true, value: { ident, etype, pos } }; + } return { ok: true, value: { ident, pos } }; } this.report("expected param"); @@ -212,8 +270,9 @@ export class Parser { const pos = this.pos(); this.step(); const paramResult = this.parseParam(); - if (!paramResult.ok) + if (!paramResult.ok) { return this.stmt({ type: "error" }, pos); + } const param = paramResult.value; if (!this.test("=")) { this.report("expected '='"); @@ -297,10 +356,25 @@ export class Parser { const subject = this.parsePrefix(); return this.expr({ type: "unary", unaryType: "not", subject }, pos); } - for (const binaryType of ["+", "*", "==", "-", "/", "!=", "<", ">", "<=", ">=", "or", "and"]) { - const subject = this.parseBinary(binaryType as BinaryType, pos) + for ( + const binaryType of [ + "+", + "*", + "==", + "-", + "/", + "!=", + "<", + ">", + "<=", + ">=", + "or", + "and", + ] + ) { + const subject = this.parseBinary(binaryType as BinaryType, pos); if (subject !== null) { - return subject + return subject; } } return this.parsePostfix(); @@ -312,8 +386,8 @@ export class Parser { const left = this.parsePrefix(); const right = this.parsePrefix(); return this.expr({ type: "binary", binaryType, left, right }, pos); - } - return null + } + return null; } public parsePostfix(): Expr { @@ -349,8 +423,9 @@ export class Parser { args.push(this.parseExpr()); while (this.test(",")) { this.step(); - if (this.test(")")) + if (this.test(")")) { break; + } args.push(this.parseExpr()); } } @@ -394,7 +469,7 @@ export class Parser { } if (this.test("null")) { this.step(); - return this.expr({ type: "null"}, pos); + return this.expr({ type: "null" }, pos); } if (this.test("(")) { this.step(); @@ -406,16 +481,78 @@ export class Parser { this.step(); return this.expr({ type: "group", expr }, pos); } - if (this.test("{")) + if (this.test("{")) { return this.parseBlock(); - if (this.test("if")) + } + if (this.test("if")) { return this.parseIf(); - if (this.test("loop")) + } + if (this.test("loop")) { return this.parseLoop(); + } this.report("expected expr", pos); this.step(); return this.expr({ type: "error" }, pos); } + public parseEType(): EType { + const pos = this.pos(); + if (this.test("ident")) { + const ident = this.current().identValue!; + return this.etype({ type: "ident", value: ident }, pos); + } + if (this.test("[")) { + this.step(); + const inner = this.parseEType(); + if (!this.test("]")) { + this.report("expected ']'", pos); + return this.etype({ type: "error" }, pos); + } + this.step(); + return this.etype({ type: "array", inner }, pos); + } + if (this.test("struct")) { + this.step(); + if (!this.test("{")) { + this.report("expected '{'"); + return this.etype({ type: "error" }, pos); + } + const fields = this.parseETypeStructFields(); + return this.etype({ type: "struct", fields }, pos); + } + this.report("expected type"); + return this.etype({ type: "error" }, pos); + } + + public parseETypeStructFields(): Param[] { + this.step(); + if (this.test("}")) { + this.step(); + return []; + } + const params: Param[] = []; + const paramResult = this.parseParam(); + if (!paramResult.ok) { + return []; + } + params.push(paramResult.value); + while (this.test(",")) { + this.step(); + if (this.test("}")) { + break; + } + const paramResult = this.parseParam(); + if (!paramResult.ok) { + return []; + } + params.push(paramResult.value); + } + if (!this.test("}")) { + this.report("expected '}'"); + return params; + } + this.step(); + return params; + } } diff --git a/compiler/ast.ts b/compiler/ast.ts index 464ff14..4a27113 100644 --- a/compiler/ast.ts +++ b/compiler/ast.ts @@ -1,59 +1,97 @@ import { Pos } from "./Token.ts"; +import { VType } from "./vtypes.ts"; export type UnaryType = "not"; -export type BinaryType = "+" | "*" | "==" | "-" | "/" | "!=" | "<" | ">" | "<=" | ">=" | "or" | "and"; +export type BinaryType = + | "+" + | "*" + | "==" + | "-" + | "/" + | "!=" + | "<" + | ">" + | "<=" + | ">=" + | "or" + | "and"; export type Param = { - ident: string, - pos: Pos, + ident: string; + etype?: EType; + pos: Pos; + vtype?: VType; }; export type Stmt = { - kind: StmtKind, - pos: Pos, - id: number, + kind: StmtKind; + pos: Pos; + vtype?: VType; + id: number; }; export type StmtKind = | { type: "error" } - | { type: "break", expr?: Expr } - | { type: "return", expr?: Expr } - | { type: "fn", ident: string, params: Param[], body: Expr } - | { type: "let", param: Param, value: Expr } - | { type: "assign", subject: Expr, value: Expr } - | { type: "expr", expr: Expr } - ; + | { type: "break"; expr?: Expr } + | { type: "return"; expr?: Expr } + | { + type: "fn"; + ident: string; + params: Param[]; + returnType?: EType; + body: Expr; + } + | { type: "let"; param: Param; value: Expr } + | { type: "assign"; subject: Expr; value: Expr } + | { type: "expr"; expr: Expr }; export type Expr = { - kind: ExprKind, - pos: Pos, - id: number, + kind: ExprKind; + pos: Pos; + vtype?: VType; + id: number; }; export type ExprKind = | { type: "error" } - | { type: "int", value: number } - | { type: "string", value: string } - | { type: "ident", value: string } - | { type: "group", expr: Expr } - | { type: "field", subject: Expr, value: string } - | { type: "index", subject: Expr, value: Expr } - | { type: "call", subject: Expr, args: Expr[] } - | { type: "unary", unaryType: UnaryType, subject: Expr } - | { type: "binary", binaryType: BinaryType, left: Expr, right: Expr } - | { type: "if", cond: Expr, truthy: Expr, falsy?: Expr } - | { type: "bool", value: boolean} - | { type: "null"} - | { type: "loop", body: Expr } - | { type: "block", stmts: Stmt[], expr?: Expr } - | { type: "sym", ident: string, defType: "let" | "fn" | "fn_param" | "builtin", stmt?: Stmt, param?: Param } - ; + | { type: "int"; value: number } + | { type: "string"; value: string } + | { type: "ident"; value: string } + | { type: "group"; expr: Expr } + | { type: "field"; subject: Expr; value: string } + | { type: "index"; subject: Expr; value: Expr } + | { type: "call"; subject: Expr; args: Expr[] } + | { type: "unary"; unaryType: UnaryType; subject: Expr } + | { type: "binary"; binaryType: BinaryType; left: Expr; right: Expr } + | { type: "if"; cond: Expr; truthy: Expr; falsy?: Expr } + | { type: "bool"; value: boolean } + | { type: "null" } + | { type: "loop"; body: Expr } + | { type: "block"; stmts: Stmt[]; expr?: Expr } + | { + type: "sym"; + ident: string; + defType: "let" | "fn" | "fn_param" | "builtin"; + stmt?: Stmt; + param?: Param; + }; export type Sym = { - ident: string, - type: "let" | "fn" | "fn_param" | "builtin", - pos?: Pos, - stmt?: Stmt, - param?: Param, -} + ident: string; + type: "let" | "fn" | "fn_param" | "builtin"; + pos?: Pos; + stmt?: Stmt; + param?: Param; +}; +export type EType = { + kind: ETypeKind; + pos: Pos; + id: number; +}; + +export type ETypeKind = + | { type: "error" } + | { type: "ident"; value: string } + | { type: "array"; inner: EType } + | { type: "struct"; fields: Param[] }; diff --git a/compiler/vtypes.ts b/compiler/vtypes.ts new file mode 100644 index 0000000..9f617ed --- /dev/null +++ b/compiler/vtypes.ts @@ -0,0 +1,63 @@ +export type VType = + | { type: "" } + | { type: "error" } + | { type: "unknown" } + | { type: "null" } + | { type: "int" } + | { type: "string" } + | { type: "bool" } + | { type: "array"; inner: VType } + | { type: "struct"; fields: VTypeParam[] } + | { type: "fn"; params: VTypeParam[]; returnType: VType }; + +export type VTypeParam = { + ident: string; + vtype: VType; +}; + +export function vtypesEqual(a: VType, b: VType): boolean { + if (a.type !== b.type) { + return false; + } + if ( + ["error", "unknown", "null", "int", "string", "bool", "struct"] + .includes(a.type) + ) { + return true; + } + if (a.type === "array" && b.type === "array") { + return vtypesEqual(a.inner, b.inner); + } + if (a.type === "fn" && b.type === "fn") { + if (a.params.length !== b.params.length) { + return false; + } + for (let i = 0; i < a.params.length; ++i) { + if (!vtypesEqual(a.params[i].vtype, b.params[i].vtype)) { + return false; + } + } + return vtypesEqual(a.returnType, b.returnType); + } + return false; +} + +export function vtypeToString(vtype: VType): string { + if ( + ["error", "unknown", "null", "int", "string", "bool", "struct"] + .includes(vtype.type) + ) { + return vtype.type; + } + if (vtype.type === "array") { + return `[${vtypeToString(vtype.inner)}]`; + } + if (vtype.type === "fn") { + const paramString = vtype.params.map((param) => + `${param.ident}: ${vtypeToString(param.vtype)}` + ) + .join(", "); + return `fn (${paramString}) -> ${vtypeToString(vtype.returnType)}`; + } + throw new Error(`unhandled vtype '${vtype.type}'`); +}