add typechecker

This commit is contained in:
SimonFJ20 2024-12-06 14:17:52 +01:00
parent aa888c9368
commit d4ea73de1d
5 changed files with 593 additions and 111 deletions

229
compiler/Checker.ts Normal file
View File

@ -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" } },
];

View File

@ -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()
if (this.test("/")) {
while (!this.done() && !this.test("\n"))
this.step();
return this.next()
if (this.test("/")) {
while (!this.done() && !this.test("\n")) {
this.step();
}
return this.token("/", pos)
return this.next();
}
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();
}
private done(): boolean { return this.index >= this.text.length; }
private done(): boolean {
return this.index >= this.text.length;
}
private current(): string { return this.text[this.index]; }
private current(): string {
return this.text[this.index];
}
public currentPos(): Pos { return this.pos(); }
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());
}
}
}

View File

@ -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,7 +174,9 @@ 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")) {
@ -161,13 +204,21 @@ 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();
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[] {
this.step();
@ -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();
@ -313,7 +387,7 @@ export class Parser {
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;
}
}

View File

@ -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[] };

63
compiler/vtypes.ts Normal file
View File

@ -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}'`);
}