From 9e6e92b5203d157784192264c8daa2dc47f926c1 Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Mon, 9 Dec 2024 13:33:33 +0100 Subject: [PATCH] test checker --- compiler/Checker.ts | 658 ++++++++++++++++++++++++++++++++++---------- compiler/ast.ts | 2 +- compiler/vtypes.ts | 1 - 3 files changed, 516 insertions(+), 145 deletions(-) diff --git a/compiler/Checker.ts b/compiler/Checker.ts index 40836be..a264257 100644 --- a/compiler/Checker.ts +++ b/compiler/Checker.ts @@ -1,11 +1,513 @@ -import { EType, Expr } from "./ast.ts"; +import { StmtKind } from "./ast.ts"; +import { EType, Expr, Stmt } 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}`); + private fnReturnStack: VType[] = []; + private loopBreakStack: VType[][] = []; + + public check(stmts: Stmt[]) { + for (const stmt of stmts) { + this.checkStmt(stmt); + } } + + public checkStmt(stmt: Stmt) { + switch (stmt.kind.type) { + case "error": + return { type: "error" }; + case "break": + return this.checkBreakStmt(stmt); + case "return": + return this.checkReturnStmt(stmt); + case "fn": + return this.checkFnStmt(stmt); + case "let": + return this.checkLetStmt(stmt); + case "assign": + return this.checkAssignStmt(stmt); + case "expr": + return this.checkExpr(stmt.kind.expr); + } + } + + public checkBreakStmt(stmt: Stmt) { + if (stmt.kind.type !== "break") { + throw new Error(); + } + const pos = stmt.pos; + if (this.loopBreakStack.length === 0) { + this.report("cannot break outside loop context", pos); + return; + } + const exprType: VType = stmt.kind.expr + ? this.checkExpr(stmt.kind.expr) + : { type: "null" }; + const breakTypes = this.loopBreakStack.at(-1)!; + if (breakTypes.length === 0) { + breakTypes.push(exprType); + return; + } + const prevBreakType = breakTypes.at(-1)!; + if (!vtypesEqual(prevBreakType, exprType)) { + this.report( + `incompatible types for break` + + `, got ${exprType}` + + ` incompatible with ${prevBreakType}`, + pos, + ); + return; + } + breakTypes.push(exprType); + } + + public checkReturnStmt(stmt: Stmt) { + if (stmt.kind.type !== "return") { + throw new Error(); + } + const pos = stmt.pos; + if (this.fnReturnStack.length === 0) { + this.report("cannot return outside fn context", pos); + return; + } + const exprType: VType = stmt.kind.expr + ? this.checkExpr(stmt.kind.expr) + : { type: "null" }; + const returnType = this.fnReturnStack.at(-1)!; + if (!vtypesEqual(exprType, returnType)) { + this.report( + `incompatible return type` + + `, got ${exprType}` + + `, expected ${returnType}`, + pos, + ); + } + } + + public checkFnStmt(stmt: Stmt) { + if (stmt.kind.type !== "fn") { + throw new Error(); + } + const pos = stmt.pos; + const returnType: VType = stmt.kind.returnType + ? this.checkEType(stmt.kind.returnType) + : { type: "null" }; + const params: VTypeParam[] = []; + for (const param of stmt.kind.params) { + if (param.etype === undefined) { + this.report("parameter types must be defined", param.pos); + stmt.kind.vtype = { type: "error" }; + return; + } + const vtype = this.checkEType(param.etype!); + param.vtype = vtype; + params.push({ ident: param.ident, vtype }); + } + stmt.kind.vtype = { type: "fn", params, returnType }; + this.fnReturnStack.push(returnType); + const body = this.checkExpr(stmt.kind.body); + this.fnReturnStack.pop(); + if (!vtypesEqual(returnType, body)) { + this.report( + `incompatible return type` + + `, got ${body}` + + `, expected ${returnType}`, + pos, + ); + } + } + + public checkLetStmt(stmt: Stmt) { + if (stmt.kind.type !== "let") { + throw new Error(); + } + const pos = stmt.pos; + const value = this.checkExpr(stmt.kind.value); + if (stmt.kind.param.etype) { + const paramVtype = this.checkEType(stmt.kind.param.etype); + if (!vtypesEqual(value, paramVtype)) { + this.report( + `incompatible value type` + + `, got '${value}'` + + `, expected '${paramVtype}'`, + pos, + ); + return; + } + } + stmt.kind.param.vtype = value; + } + + public checkAssignStmt(stmt: Stmt) { + if (stmt.kind.type !== "assign") { + throw new Error(); + } + const pos = stmt.pos; + const value = this.checkExpr(stmt.kind.value); + switch (stmt.kind.subject.kind.type) { + case "field": { + const subject = this.checkExpr(stmt.kind.subject.kind.subject); + if (subject.type !== "struct") { + this.report("cannot use field on non-struct", pos); + return { type: "error" }; + } + const fieldValue = stmt.kind.subject.kind.value; + const found = subject.fields.find((param) => + param.ident === fieldValue + ); + if (!found) { + this.report( + `no field named '${stmt.kind.subject.kind.value}' on struct`, + pos, + ); + return { type: "error" }; + } + if (!vtypesEqual(found.vtype, value)) { + this.report( + `cannot assign incompatible type to field '${found.ident}'` + + `, got '${vtypeToString(value)}'` + + `, expected '${vtypeToString(found.vtype)}'`, + pos, + ); + return; + } + return; + } + case "index": { + const subject = this.checkExpr(stmt.kind.subject.kind.subject); + if (subject.type !== "array") { + this.report("cannot index on non-array", pos); + return { type: "error" }; + } + const indexValue = this.checkExpr(stmt.kind.subject.kind.value); + if (indexValue.type !== "int") { + this.report("cannot index on array with non-int", pos); + return { type: "error" }; + } + if (!vtypesEqual(subject.inner, value)) { + this.report( + `cannot assign incompatible type to array ` + + `'${vtypeToString(subject)}'` + + `, got '${vtypeToString(value)}'`, + pos, + ); + return; + } + return; + } + case "sym": { + if (stmt.kind.subject.kind.defType !== "let") { + this.report("cannot only assign to let-symbol", pos); + return { type: "error" }; + } + if (!vtypesEqual(stmt.kind.subject.kind.param!.vtype!, value)) { + this.report( + `cannot assign to incompatible type` + + `, got '${vtypeToString(value)}'` + + `, expected '${ + vtypeToString( + stmt.kind.subject.kind.param!.vtype!, + ) + }'`, + pos, + ); + return; + } + return; + } + default: + this.report("unassignable expression", pos); + return; + } + } + + public checkExpr(expr: Expr): VType { + 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 "group": + return this.checkExpr(expr.kind.expr); + case "field": + return this.checkFieldExpr(expr); + case "index": + return this.checkIndexExpr(expr); + case "call": + return this.checkCallExpr(expr); + case "unary": + return this.checkUnaryExpr(expr); + case "binary": + return this.checkBinaryExpr(expr); + case "if": + return this.checkIfExpr(expr); + case "loop": + return this.checkLoopExpr(expr); + case "block": + return this.checkBlockExpr(expr); + } + // throw new Error(`unhandled type ${expr.kind.type}`); + })(); + return expr.vtype = vtype; + } + + public checkSymExpr(expr: Expr): VType { + if (expr.kind.type !== "sym") { + throw new Error(); + } + const pos = expr.pos; + switch (expr.kind.defType) { + case "let": + return expr.kind.param?.vtype!; + case "fn": { + const fnStmt = expr.kind.stmt!; + if (fnStmt.kind.type !== "fn") { + throw new Error(); + } + const vtype = fnStmt.kind.vtype!; + if (vtype.type !== "fn") { + throw new Error(); + } + const { params, returnType } = vtype; + return { type: "fn", params, returnType }; + } + case "fn_param": + return expr.kind.param!.vtype!; + case "builtin": + throw new Error(); + } + } + + public checkFieldExpr(expr: Expr): VType { + if (expr.kind.type !== "field") { + throw new Error(); + } + const pos = expr.pos; + 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; + } + + public checkIndexExpr(expr: Expr): VType { + if (expr.kind.type !== "index") { + throw new Error(); + } + const pos = expr.pos; + const subject = this.checkExpr(expr.kind.subject); + if (subject.type !== "array") { + this.report("cannot index on non-array", pos); + return { type: "error" }; + } + const value = this.checkExpr(expr.kind.value); + if (value.type !== "int") { + this.report("cannot index on array with non-int", pos); + return { type: "error" }; + } + return subject.inner; + } + + public checkCallExpr(expr: Expr): VType { + if (expr.kind.type !== "call") { + throw new Error(); + } + const pos = expr.pos; + 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; + } + + public checkUnaryExpr(expr: Expr): VType { + if (expr.kind.type !== "unary") { + throw new Error(); + } + const pos = expr.pos; + const subject = this.checkExpr(expr.kind.subject); + for (const operation of simpleUnaryOperations) { + if (operation.unaryType !== expr.kind.unaryType) { + continue; + } + if (!vtypesEqual(operation.operand, subject)) { + continue; + } + return operation.result ?? operation.operand; + } + this.report( + `cannot apply unary operation '${expr.kind.unaryType}' ` + + `on type '${vtypeToString(subject)}'`, + pos, + ); + return { type: "error" }; + } + + public checkBinaryExpr(expr: Expr): VType { + if (expr.kind.type !== "binary") { + throw new Error(); + } + const pos = expr.pos; + 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" }; + } + + public checkIfExpr(expr: Expr): VType { + if (expr.kind.type !== "if") { + throw new Error(); + } + const pos = expr.pos; + const cond = this.checkExpr(expr.kind.cond); + const truthy = this.checkExpr(expr.kind.truthy); + const falsy = expr.kind.falsy + ? this.checkExpr(expr.kind.falsy) + : undefined; + if (cond.type !== "bool") { + this.report( + `if condition should be 'bool', got '${vtypeToString(cond)}'`, + pos, + ); + return { type: "error" }; + } + if (falsy === undefined && truthy.type !== "null") { + this.report( + `if expressions without false-case must result in type 'null'` + + `, got '${vtypeToString(truthy)}'`, + pos, + ); + return { type: "error" }; + } + if (falsy !== undefined && !vtypesEqual(truthy, falsy)) { + this.report( + `if cases must be compatible, got incompatible types` + + ` '${vtypeToString(truthy)}'` + + ` and '${vtypeToString(falsy)}'`, + pos, + ); + return { type: "error" }; + } + return truthy; + } + + public checkLoopExpr(expr: Expr): VType { + if (expr.kind.type !== "loop") { + throw new Error(); + } + const pos = expr.pos; + this.loopBreakStack.push([]); + const body = this.checkExpr(expr.kind.body); + if (body.type !== "null") { + this.report( + `loop body must result in type 'null'` + + `, got '${vtypeToString(body)}'`, + pos, + ); + return { type: "error" }; + } + const loopBreakTypes = this.loopBreakStack.pop()!; + if (loopBreakTypes.length === 0) { + return { type: "null" }; + } + const breakType = loopBreakTypes.reduce<[VType, boolean, VType]>( + (acc, curr) => { + const [resulting, isIncompatible, outlier] = acc; + if (isIncompatible) { + return acc; + } + if (!vtypesEqual(resulting, curr)) { + return [resulting, true, curr]; + } + return [resulting, false, outlier]; + }, + [{ type: "null" }, false, { type: "null" }], + ); + if (breakType[1]) { + this.report( + `incompatible types in break statements` + + `, got '${vtypeToString(breakType[2])}'` + + ` incompatible with ${vtypeToString(breakType[0])}`, + pos, + ); + return { type: "error" }; + } + return breakType[0]; + } + + public checkBlockExpr(expr: Expr): VType { + if (expr.kind.type !== "block") { + throw new Error(); + } + const pos = expr.pos; + for (const stmt of expr.kind.stmts) { + this.checkStmt(stmt); + } + return expr.kind.expr + ? this.checkExpr(expr.kind.expr) + : { type: "null" }; + } + public checkEType(etype: EType): VType { const pos = etype.pos; if (etype.kind.type === "ident") { @@ -69,149 +571,19 @@ export class Checker { 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": - return this.checkFieldExpr(expr); - case "index": - return this.checkIndexExpr(expr); - case "call": - return this.checkCallExpr(expr); - 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 { - if (expr.kind.type !== "binary") { - throw new Error(); - } - const pos = expr.pos; - 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" }; - } - - public checkFieldExpr(expr: Expr): VType { - if (expr.kind.type !== "field") { - throw new Error(); - } - const pos = expr.pos; - 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; - } - - public checkIndexExpr(expr: Expr): VType { - if (expr.kind.type !== "index") { - throw new Error(); - } - const pos = expr.pos; - 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; - } - - public checkCallExpr(expr: Expr): VType { - if (expr.kind.type !== "call") { - throw new Error(); - } - const pos = expr.pos; - 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; + private report(msg: string, pos: Pos) { + console.error(`${msg} at ${pos.line}:${pos.col}`); } } +const simpleUnaryOperations: { + unaryType: string; + operand: VType; + result?: VType; +}[] = [ + { unaryType: "not", operand: { type: "bool" } }, +]; + const simpleBinaryOperations: { binaryType: string; operand: VType; diff --git a/compiler/ast.ts b/compiler/ast.ts index 4a27113..05255f9 100644 --- a/compiler/ast.ts +++ b/compiler/ast.ts @@ -26,7 +26,6 @@ export type Param = { export type Stmt = { kind: StmtKind; pos: Pos; - vtype?: VType; id: number; }; @@ -40,6 +39,7 @@ export type StmtKind = params: Param[]; returnType?: EType; body: Expr; + vtype?: VType; } | { type: "let"; param: Param; value: Expr } | { type: "assign"; subject: Expr; value: Expr } diff --git a/compiler/vtypes.ts b/compiler/vtypes.ts index 4567c0c..cb92482 100644 --- a/compiler/vtypes.ts +++ b/compiler/vtypes.ts @@ -1,5 +1,4 @@ export type VType = - | { type: "" } | { type: "error" } | { type: "unknown" } | { type: "null" }