test checker

This commit is contained in:
SimonFJ20 2024-12-09 13:33:33 +01:00
parent 5b4057160d
commit 9e6e92b520
3 changed files with 516 additions and 145 deletions

View File

@ -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 { Pos } from "./Token.ts";
import { VType, VTypeParam, vtypesEqual, vtypeToString } from "./vtypes.ts"; import { VType, VTypeParam, vtypesEqual, vtypeToString } from "./vtypes.ts";
export class Checker { export class Checker {
public report(msg: string, pos: Pos) { private fnReturnStack: VType[] = [];
console.error(`${msg} at ${pos.line}:${pos.col}`); 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 { public checkEType(etype: EType): VType {
const pos = etype.pos; const pos = etype.pos;
if (etype.kind.type === "ident") { if (etype.kind.type === "ident") {
@ -69,149 +571,19 @@ export class Checker {
throw new Error(`unknown explicit type ${etype.kind.type}`); throw new Error(`unknown explicit type ${etype.kind.type}`);
} }
public checkExpr(expr: Expr): VType { private report(msg: string, pos: Pos) {
const pos = expr.pos; console.error(`${msg} at ${pos.line}:${pos.col}`);
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;
} }
} }
const simpleUnaryOperations: {
unaryType: string;
operand: VType;
result?: VType;
}[] = [
{ unaryType: "not", operand: { type: "bool" } },
];
const simpleBinaryOperations: { const simpleBinaryOperations: {
binaryType: string; binaryType: string;
operand: VType; operand: VType;

View File

@ -26,7 +26,6 @@ export type Param = {
export type Stmt = { export type Stmt = {
kind: StmtKind; kind: StmtKind;
pos: Pos; pos: Pos;
vtype?: VType;
id: number; id: number;
}; };
@ -40,6 +39,7 @@ export type StmtKind =
params: Param[]; params: Param[];
returnType?: EType; returnType?: EType;
body: Expr; body: Expr;
vtype?: VType;
} }
| { type: "let"; param: Param; value: Expr } | { type: "let"; param: Param; value: Expr }
| { type: "assign"; subject: Expr; value: Expr } | { type: "assign"; subject: Expr; value: Expr }

View File

@ -1,5 +1,4 @@
export type VType = export type VType =
| { type: "" }
| { type: "error" } | { type: "error" }
| { type: "unknown" } | { type: "unknown" }
| { type: "null" } | { type: "null" }