more type checker
All checks were successful
Check / Explore-Gitea-Actions (push) Successful in 8s

This commit is contained in:
sfja 2026-04-15 17:12:05 +02:00
parent db129a3c16
commit 9e24ff3002
8 changed files with 563 additions and 40 deletions

View File

@ -170,7 +170,8 @@ export type IntTy =
| "u16"
| "u32"
| "u64"
| "usize";
| "usize"
| "any";
export type UnaryOp =
| "Not"

View File

@ -256,6 +256,8 @@ class ExprChecker {
return Ty.U32;
case "u64":
return Ty.U64;
case "usize":
return Ty.USize;
case "i8":
return Ty.U8;
case "i16":
@ -264,6 +266,10 @@ class ExprChecker {
return Ty.I32;
case "i64":
return Ty.U64;
case "isize":
return Ty.ISize;
case "any":
return Ty.I32;
default:
throw new Error(`intType '${node.kind.intTy}' not handled`);
}

View File

@ -2,6 +2,7 @@ import { Syms } from "./resolve.ts";
import * as ast from "../ast.ts";
import { Ty } from "../ty.ts";
import { FileReporter, Loc } from "../diagnostics.ts";
import { exit } from "node:process";
export function checkFn(
fn: ast.FnStmt,
@ -57,12 +58,10 @@ class TypeChecker {
this.fn.kind.body.visit({
visit: (node) => {
if (!node.is("ReturnStmt")) {
return;
}
const ty = node.kind.expr
? this.expr(node.kind.expr, retTy)
: Ty.Void;
const k = node.kind;
switch (k.tag) {
case "ReturnStmt": {
const ty = k.expr ? this.expr(k.expr, retTy) : Ty.Void;
if (!ty.resolvableWith(retTy)) {
this.reporter.error(
node.loc,
@ -74,6 +73,93 @@ class TypeChecker {
);
this.reporter.abort();
}
break;
}
case "LetStmt": {
let ty: Ty;
const paramTy = k.param.as("Param").kind.ty;
if (paramTy) {
const explicitTy = this.ty(paramTy);
const exprTy = this.expr(k.expr, explicitTy);
const res = this.resolve(
exprTy,
explicitTy,
k.expr.loc,
);
if (res.rewriteSubtree) {
this.rewriteTree(k.expr, res.ty);
}
ty = res.ty;
} else {
ty = this.expr(k.expr, Ty.Any);
}
this.nodeTys.set(node, ty);
break;
}
case "ExprStmt": {
this.expr(k.expr, Ty.Any);
break;
}
case "AssignStmt": {
const placeTy = this.expr(k.place, Ty.Any);
const exprTy = this.expr(k.expr, Ty.Any);
if (!placeTy.resolvableWith(exprTy)) {
this.reporter.error(
k.expr.loc,
`type '${exprTy.pretty()}' not assignable to type '${placeTy.pretty()}'`,
);
this.reporter.abort();
}
break;
}
case "IfStmt": {
const condTy = this.expr(k.cond, Ty.Bool);
if (!condTy.is("Bool")) {
this.reporter.error(
k.cond.loc,
`expected condition to be 'bool', got '${condTy.pretty()}'`,
);
this.reporter.abort();
}
break;
}
case "WhileStmt": {
const condTy = this.expr(k.cond, Ty.Bool);
if (!condTy.is("Bool")) {
this.reporter.error(
k.cond.loc,
`expected condition to be 'bool', got '${condTy.pretty()}'`,
);
this.reporter.abort();
}
break;
}
case "BreakStmt": {
break;
}
case "FnStmt":
case "Error":
case "File":
case "Block":
case "Param":
case "IdentExpr":
case "IntExpr":
case "StrExpr":
case "ArrayExpr":
case "IndexExpr":
case "CallExpr":
case "UnaryExpr":
case "BinaryExpr":
case "RangeExpr":
case "IdentTy":
case "PtrTy":
case "PtrMutTy":
case "ArrayTy":
case "SliceTy":
break;
default:
k satisfies never;
}
},
});
@ -111,10 +197,64 @@ class TypeChecker {
case "SliceTy":
throw new Error(`node '${k.tag}' not an expression`);
case "IdentExpr": {
throw new Error("not implemented");
const sym = this.syms.get(expr);
if (sym.tag === "Fn") {
throw new Error("todo");
}
if (sym.tag === "Bool") {
return Ty.Bool;
}
if (sym.tag === "Builtin") {
this.reporter.error(
expr.loc,
`invalid use of builtin '${sym.id}'`,
);
this.reporter.abort();
}
if (sym.tag === "FnParam") {
return this.expr(sym.param, Ty.Any);
}
if (sym.tag === "Let") {
const ty = this.nodeTys.get(sym.stmt);
if (!ty) {
throw new Error();
}
return ty;
}
throw new Error(`'${sym.tag}' not handled`);
}
case "IntExpr": {
return this.resolve(Ty.AnyInt, expected, expr.loc).ty;
const intTy = (() => {
switch (k.intTy) {
case "u8":
return Ty.U8;
case "u16":
return Ty.U16;
case "u32":
return Ty.U32;
case "u64":
return Ty.U64;
case "usize":
return Ty.ISize;
case "i8":
return Ty.U8;
case "i16":
return Ty.U16;
case "i32":
return Ty.I32;
case "i64":
return Ty.U64;
case "isize":
return Ty.ISize;
case "any":
return Ty.AnyInt;
default:
throw new Error(
`intType '${k.intTy}' not handled`,
);
}
})();
return this.resolve(intTy, expected, expr.loc).ty;
}
case "StrExpr": {
return this.resolve(
@ -168,19 +308,191 @@ class TypeChecker {
return res.ty;
}
case "IndexExpr": {
throw new Error("not implemented");
const innerTy = this.expr(
k.value,
Ty.Indexable(expected),
);
if (!innerTy.isIndexable()) {
this.reporter.error(
expr.loc,
`expected indexable type, got '${expected.pretty()}'`,
);
this.reporter.abort();
}
let argTy = this.expr(k.arg, Ty.Any);
if (argTy.is("AnyInt")) {
argTy = Ty.USize;
this.rewriteTree(k.arg, argTy);
return innerTy.indexableTy()!;
} else if (argTy.is("Range")) {
return Ty.Slice(innerTy.indexableTy()!);
} else {
throw new Error();
}
}
case "CallExpr": {
throw new Error("not implemented");
const checkArgs = (
callableTy: Ty & { kind: { tag: "Fn" } },
) => {
const params = callableTy.kind.params;
if (k.args.length !== params.length) {
this.reporter.error(
expr.loc,
`incorrect amount of arguments. got ${k.args.length} expected ${params.length}`,
);
if (calleeTy?.is("FnStmt")) {
this.reporter.info(
calleeTy.kind.stmt.loc,
"function defined here",
);
}
this.reporter.abort();
}
const args = k.args
.map((arg, i) => this.expr(arg, params[i]));
for (const i of args.keys()) {
this.resolve(args[i], params[i], k.args[i].loc, () => {
if (calleeTy?.is("FnStmt")) {
this.reporter.info(
calleeTy.kind.stmt.kind.params[i].loc,
`parameter '${
calleeTy.kind.stmt.kind.params[i]
.as("Param").kind.ident
}' defined here`,
);
}
this.reporter.error(
k.args[i].loc,
`type '${
args[i].pretty()
}' not compatible with type '${
params[i].pretty()
}', for argument ${i}`,
);
});
}
};
const sym = k.value.is("IdentExpr")
? this.syms.get(k.value)
: null;
if (sym?.tag === "Builtin") {
if (sym.id === "len") {
checkArgs(
Ty.create("Fn", {
params: [Ty.Any],
retTy: Ty.USize,
}).as("Fn"),
);
return Ty.USize;
}
if (sym.id === "print") {
void k.args
.map((arg) => this.expr(arg, Ty.Any));
return Ty.Void;
}
throw new Error(`builtin '${sym.id}' not handled`);
}
const calleeTy = this.expr(k.value, Ty.Callable(expected));
if (!calleeTy.isCallable()) {
this.reporter.error(
expr.loc,
`expected callable type, got '${expected.pretty()}'`,
);
this.reporter.abort();
}
const callableTy = calleeTy.callableTy();
checkArgs(callableTy);
return callableTy.kind.retTy;
}
case "UnaryExpr": {
throw new Error("not implemented");
switch (k.op) {
case "Not": {
const ty = this.expr(k.expr, Ty.Bool);
if (!ty.is("Bool")) {
this.reporter.error(
expr.loc,
`expected 'bool', got '${expected.pretty()}'`,
);
this.reporter.abort();
}
return ty;
}
case "Neg": {
const ty = this.expr(k.expr, expected);
if (!ty.is("Int")) {
this.reporter.error(
expr.loc,
`cannot apply ! to '${expected.pretty()}'`,
);
this.reporter.abort();
}
return ty;
}
case "Ref": {
const ty = this.expr(k.expr, expected);
return Ty.Ptr(ty);
}
case "RefMut": {
const ty = this.expr(k.expr, expected);
return Ty.Ptr(ty);
}
case "Deref": {
const ty = this.expr(k.expr, expected);
if (!ty.is("Ptr") && !ty.is("PtrMut")) {
this.reporter.error(
expr.loc,
`cannot dereference type '${expected.pretty()}'`,
);
this.reporter.abort();
}
if (!ty.kind.ty.isSized()) {
this.reporter.error(
expr.loc,
`cannot dereference unsized type '${ty.kind.ty.pretty()}' in an expression`,
);
this.reporter.abort();
}
return ty.kind.ty;
}
default:
k satisfies never;
throw new Error();
}
}
case "BinaryExpr": {
throw new Error("not implemented");
const left = this.expr(k.left, expected);
const right = this.expr(k.right, expected);
const result = binaryOpTests
.map((test) => test(k.op, left, right))
.filter((result) => result)
.at(0);
if (!result) {
this.reporter.error(
expr.loc,
`operator '${k.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`,
);
this.reporter.abort();
}
return result;
}
case "RangeExpr": {
throw new Error("not implemented");
for (const operandExpr of [k.begin, k.end]) {
const operandTy = operandExpr &&
this.expr(operandExpr, Ty.USize);
if (operandTy && !operandTy.resolvableWith(Ty.I32)) {
this.reporter.error(
operandExpr.loc,
`range operand must be '${Ty.I32.pretty()}', not '${operandTy.pretty()}'`,
);
this.reporter.abort();
}
}
return Ty.create("Range", {});
}
default:
k satisfies never;
@ -193,10 +505,103 @@ class TypeChecker {
}
private checkTy(ty: ast.Node): Ty {
throw new Error("not implemented");
const k = ty.kind;
switch (k.tag) {
case "Error":
case "File":
case "Block":
case "ExprStmt":
case "AssignStmt":
case "FnStmt":
case "ReturnStmt":
case "LetStmt":
case "IfStmt":
case "WhileStmt":
case "BreakStmt":
case "Param":
case "IdentExpr":
case "IntExpr":
case "StrExpr":
case "ArrayExpr":
case "IndexExpr":
case "CallExpr":
case "UnaryExpr":
case "BinaryExpr":
case "RangeExpr":
throw new Error(`node '${k.tag}' not a type`);
case "IdentTy": {
const sym = this.syms.get(ty);
if (sym.tag === "BuiltinTy") {
switch (sym.ident) {
case "void":
return Ty.Void;
case "bool":
return Ty.Bool;
case "i8":
return Ty.I8;
case "i16":
return Ty.I16;
case "i32":
return Ty.I32;
case "i64":
return Ty.I64;
case "isize":
return Ty.ISize;
case "u8":
return Ty.U8;
case "u16":
return Ty.U16;
case "u32":
return Ty.U32;
case "u64":
return Ty.U64;
case "usize":
return Ty.USize;
default:
throw new Error(
`unknown type '${k.ident}'`,
);
}
}
this.reporter.error(ty.loc, `symbol is not a type`);
return this.reporter.abort();
}
case "PtrTy": {
const ty = this.ty(k.ty);
return Ty.create("Ptr", { ty });
}
case "PtrMutTy": {
const ty = this.ty(k.ty);
return Ty.create("PtrMut", { ty });
}
case "ArrayTy": {
const ty = this.ty(k.ty);
const lengthTy = this.expr(k.length, Ty.USize);
if (!lengthTy.resolvableWith(Ty.I32)) {
this.reporter.error(
k.length.loc,
`for array length, expected 'int', got '${lengthTy.pretty()}'`,
);
this.reporter.abort();
}
if (!k.length.is("IntExpr")) {
this.reporter.error(
k.length.loc,
`array length must be an 'int' expression`,
);
this.reporter.abort();
}
const length = k.length.kind.value;
return Ty.create("Array", { ty, length });
}
case "SliceTy": {
const ty = this.ty(k.ty);
return Ty.create("Slice", { ty });
}
}
}
private cachedCheck(node: ast.Node, func: () => Ty) {
private cachedCheck(node: ast.Node, func: () => Ty): Ty {
const existing = this.nodeTys.get(node);
if (existing !== undefined) {
return existing;
@ -206,15 +611,32 @@ class TypeChecker {
return ty;
}
private resolve(ty: Ty, expected: Ty, loc: Loc): TyRes {
private resolve(
ty: Ty,
expected: Ty,
loc: Loc,
inCaseOfError?: () => void,
): TyRes {
if (ty == expected) {
return { ty, rewriteSubtree: false };
}
if (expected.is("Any")) {
return { ty, rewriteSubtree: false };
}
if (!ty.resolvableWith(expected)) {
if (inCaseOfError) {
inCaseOfError();
} else {
this.reporter.error(
loc,
`expected type '${expected.pretty()}', got '${ty.pretty()}'`,
);
}
this.reporter.abort();
}
throw new Error("not implemented");
throw new Error(
`resolving '${ty.pretty()}' with '${expected.pretty()}' not implemented`,
);
}
private rewriteTree(node: ast.Node, ty: Ty) {
@ -230,3 +652,32 @@ type TyRes = {
ty: Ty;
rewriteSubtree: boolean;
};
type BinaryOpTest = (op: ast.BinaryOp, left: Ty, right: Ty) => Ty | null;
const binaryOpTests: BinaryOpTest[] = [
(op, left, right) => {
const ops: ast.BinaryOp[] = [
"Add",
"Sub",
"Mul",
"Div",
"Rem",
];
if (
ops.includes(op) && left.is("Int") && left.resolvableWith(right)
) {
return left;
}
return null;
},
(op, left, right) => {
const ops: ast.BinaryOp[] = ["Eq", "Ne", "Lt", "Gt", "Lte", "Gte"];
if (
ops.includes(op) && left.is("Int") && left.resolvableWith(right)
) {
return Ty.Bool;
}
return null;
},
];

View File

@ -292,7 +292,7 @@ export class Parser {
throw new Error();
}
const value = Number(match[1]);
const intTy = match[2] ?? "i32";
const intTy = match[2];
if (
intTy &&
!["8", "16", "32", "64", "size"].includes(intTy.slice(1))
@ -306,7 +306,7 @@ export class Parser {
this.step();
return ast.Node.create(loc, "IntExpr", {
value,
intTy: intTy as ast.IntTy ?? "i32",
intTy: intTy as ast.IntTy ?? "any",
});
} else if (this.test("str")) {
const value = this.current.value;

View File

@ -15,7 +15,6 @@ const fileRep = reporter.ofFile({ filename, text });
const fileAst = front.parse(text, fileRep);
const syms = front.resolve(fileAst, fileRep);
const tys = new front.Tys(syms, fileRep);
let mainFn: ast.NodeWithKind<"FnStmt"> | null = null;
@ -38,6 +37,37 @@ if (!mainFn) {
Deno.exit(1);
}
const tys = new front.Tys(syms, fileRep);
// const fnTys = front.checkFn(mainFn, syms, fileRep);
//
// fileAst.visit({
// visit(node) {
// switch (node.kind.tag) {
// // case "IdentExpr":
// case "IntExpr": // case "StrExpr":
// // case "ArrayExpr":
// // case "IndexExpr":
// // case "CallExpr":
// // case "UnaryExpr":
// // case "BinaryExpr":
// // case "RangeExpr":
// {
// const oldTy = tys.expr(node);
// const newTy = fnTys.exprTy(node);
// if (oldTy !== newTy) {
// if (newTy.is("AnyInt") && oldTy.is("Int")) {
// break;
// }
// throw new Error(
// `'${newTy.pretty()}' != '${oldTy.pretty()}'`,
// );
// }
// break;
// }
// }
// },
// });
const m = new middle.MiddleLowerer(syms, tys);
const mainMiddleFn = m.lowerFn(mainFn);

View File

@ -342,6 +342,8 @@ export class FnInterpreter {
case "u64":
case "usize":
return value;
case "any":
throw new Error();
}
})(value),
});

View File

@ -104,7 +104,7 @@ export function tyPretty(ty: ty.Ty, colors = noColors): string {
case "AnyInt":
return `${c.typeIdent}{integer}`;
}
return "<unhandled>";
return `{${ty.kind.tag}}`;
}
export function mirFnPretty(fn: mir.Fn, colors = noColors): string {
@ -222,7 +222,11 @@ class MirFnPrettyStringifier {
case "Void":
return expr(``);
case "Int":
return expr(` ${c.literal}${k.value}${k.intTy}`);
return expr(
` ${c.literal}${k.value}${
k.intTy !== "any" ? k.intTy : ""
}`,
);
case "Bool":
return expr(` ${c.literal}${k.value}`);
case "Str":

View File

@ -53,6 +53,14 @@ export class Ty {
static Any = Ty.create("Any", {});
/** Only used in type checker. */
static AnyInt = Ty.create("AnyInt", {});
/** Only used in type checker. */
static Indexable(ty: Ty): Ty {
return this.create("Indexable", { ty });
}
/** Only used in type checker. */
static Callable(ty: Ty): Ty {
return this.create("Callable", { ty });
}
private internHash(): string {
return JSON.stringify(this.kind);
@ -177,13 +185,32 @@ export class Ty {
return true;
}
innerFn(): Ty & { kind: { tag: "Fn" } } {
isIndexable(): boolean {
return this.is("Array") || this.is("Slice") || this.is("Indexable");
}
indexableTy(): Ty | null {
if (this.is("Array") || this.is("Slice") || this.is("Indexable")) {
return this.kind.ty;
}
return null;
}
isCallable(): boolean {
return this.is("FnStmt") || this.is("Fn");
}
callableTy(): Ty & { kind: { tag: "Fn" } } {
if (this.is("Fn")) {
return this;
}
this.assertIs("FnStmt");
return this.kind.ty.as("Fn");
}
isCheckerInternal(): boolean {
return this.is("Any") || this.is("AnyInt");
return this.is("Any") || this.is("AnyInt") || this.is("Indexable") ||
this.is("Callable");
}
pretty(colors?: stringify.PrettyColors): string {
@ -203,4 +230,6 @@ export type TyKind =
| { tag: "Range" }
| { tag: "Fn"; params: Ty[]; retTy: Ty }
| { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> }
| { tag: "Any" | "AnyInt" };
| { tag: "Any" | "AnyInt" }
| { tag: "Indexable"; ty: Ty }
| { tag: "Callable"; ty: Ty };