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