From 0d57cd7e3cc49e662c2554e045ebba9088edea39 Mon Sep 17 00:00:00 2001 From: sfja Date: Mon, 23 Mar 2026 00:34:07 +0100 Subject: [PATCH] add i32, etc. and some other stuff --- src/ast.ts | 14 +- src/front/check.ts | 823 ++++++++++++++++++++++++----------------- src/front/parse.ts | 32 +- src/front/resolve.ts | 16 +- src/middle.ts | 86 +++-- src/mir.ts | 3 +- src/mir_interpreter.ts | 30 +- src/ty.ts | 34 +- tests/array.ethlang | 10 +- tests/assign.ethlang | 2 +- tests/fn_int.ethlang | 2 +- tests/int.ethlang | 12 +- tests/let.ethlang | 2 +- tests/pointer.ethlang | 6 +- 14 files changed, 653 insertions(+), 419 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index c676eaa..82cd512 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -142,7 +142,7 @@ export type NodeKind = | { tag: "BreakStmt" } | { tag: "Param"; ident: string; ty: Node | null } | { tag: "IdentExpr"; ident: string } - | { tag: "IntExpr"; value: number } + | { tag: "IntExpr"; value: number; intTy: IntTy } | { tag: "StrExpr"; value: string } | { tag: "ArrayExpr"; values: Node[] } | { tag: "IndexExpr"; value: Node; arg: Node } @@ -160,6 +160,18 @@ export type NodeKind = | { tag: "ArrayTy"; ty: Node; length: Node } | { tag: "SliceTy"; ty: Node }; +export type IntTy = + | "i8" + | "i16" + | "i32" + | "i64" + | "isize" + | "u8" + | "u16" + | "u32" + | "u64" + | "usize"; + export type UnaryOp = | "Not" | "Negate" diff --git a/src/front/check.ts b/src/front/check.ts index ab1040d..28cb47b 100644 --- a/src/front/check.ts +++ b/src/front/check.ts @@ -1,76 +1,137 @@ import * as ast from "../ast.ts"; import { FileReporter, Loc } from "../diagnostics.ts"; import { Ty } from "../ty.ts"; -import { Syms } from "./resolve.ts"; +import { Sym, Syms } from "./resolve.ts"; export class Tys { - private nodeTys = new Map(); - private checker: Checker; - constructor( private syms: Syms, private reporter: FileReporter, ) { - this.checker = new Checker(this, this.syms, this.reporter); + this.cx = new CheckerCx(this.syms, this.reporter); } + private cx: CheckerCx; + fnStmt(node: ast.NodeWithKind<"FnStmt">): Ty { - if (this.nodeTys.has(node.id)) { - return this.nodeTys.get(node.id)!; - } - const ty = this.checker.checkFnStmt(node); - this.nodeTys.set(node.id, ty); - return ty; + return this.cx.fnStmt(node); + } + + param(node: ast.NodeWithKind<"Param">): Ty { + return this.cx.param(node); } place(node: ast.Node): Ty { - if (this.nodeTys.has(node.id)) { - return this.nodeTys.get(node.id)!; - } - const ty = this.checker.checkPlace(node); - this.nodeTys.set(node.id, ty); - return ty; + return this.cx.place(node); } expr(node: ast.Node): Ty { - if (this.nodeTys.has(node.id)) { - return this.nodeTys.get(node.id)!; - } - const ty = this.checker.checkExpr(node); - this.nodeTys.set(node.id, ty); - return ty; + return this.cx.expr(node); + } + + ty(node: ast.Node): Ty { + return this.cx.ty(node); } } -class Checker { +class CheckerCx { constructor( - private tys: Tys, private syms: Syms, private reporter: FileReporter, ) {} + private nodeTys = new Map(); + + private stmtChecker = new StmtChecker(this); + private paramChecker = new ParamChecker(this); + private placeChecker = new PlaceChecker(this); + private exprChecker = new ExprChecker(this); + private tyChecker = new TyChecker(this); + + fnStmt(node: ast.NodeWithKind<"FnStmt">): Ty { + return this.cache(node, () => this.stmtChecker.checkFnStmt(node)); + } + + param(node: ast.NodeWithKind<"Param">): Ty { + return this.cache(node, () => this.paramChecker.checkParam(node)); + } + + place(node: ast.Node): Ty { + return this.cache(node, () => this.placeChecker.checkPlace(node)); + } + + expr(node: ast.Node): Ty { + return this.cache(node, () => this.exprChecker.checkExpr(node)); + } + + ty(node: ast.Node): Ty { + return this.cache(node, () => this.tyChecker.checkTy(node)); + } + + private cache(node: ast.Node, action: () => Ty): Ty { + if (this.nodeTys.has(node.id)) { + return this.nodeTys.get(node.id)!; + } + const ty = action(); + this.nodeTys.set(node.id, ty); + return ty; + } + + error(loc: Loc, message: string) { + this.reporter.error(loc, message); + } + + info(loc: Loc, message: string) { + this.reporter.info(loc, message); + } + + fail(): never { + this.reporter.abort(); + } + + sym(node: ast.Node): Sym { + return this.syms.get(node); + } + + assertCompatible(left: Ty, right: Ty, loc: Loc): void { + if (!left.compatibleWith(right)) { + this.error( + loc, + `type '${left.pretty()}' not compatible with type '${right.pretty()}'`, + ); + this.fail(); + } + } +} + +class StmtChecker { + constructor( + private cx: CheckerCx, + ) {} + checkFnStmt(stmt: ast.NodeWithKind<"FnStmt">): Ty { const k = stmt.kind; - const params = k.params.map((param) => this.tys.expr(param)); - const retTy = k.retTy ? this.tys.expr(k.retTy) : Ty.Void; + const params = k.params + .map((param) => this.cx.param(param.as("Param"))); + const retTy = k.retTy ? this.cx.ty(k.retTy) : Ty.Void; k.body.visit({ visit: (node) => { if (node.is("ReturnStmt")) { const ty = node.kind.expr - ? this.tys.expr(node.kind.expr) + ? this.cx.expr(node.kind.expr) : Ty.Void; if (!ty.compatibleWith(retTy)) { - this.error( + this.cx.error( node.loc, `type '${ty.pretty()}' not compatible with return type '${retTy.pretty()}'`, ); - this.info( + this.cx.info( stmt.kind.retTy?.loc ?? stmt.loc, `return type '${retTy}' defined here`, ); - this.fail(); + this.cx.fail(); } } }, @@ -79,6 +140,44 @@ class Checker { const ty = Ty.create("Fn", { params, retTy }); return Ty.create("FnStmt", { stmt, ty }); } +} + +class ParamChecker { + constructor( + private cx: CheckerCx, + ) {} + + checkParam(node: ast.NodeWithKind<"Param">): Ty { + const sym = this.cx.sym(node); + + if (sym.tag === "Let") { + const exprTy = this.cx.expr(sym.stmt.kind.expr); + if (node.kind.ty) { + const explicitTy = this.cx.ty(node.kind.ty); + this.cx.assertCompatible( + exprTy, + explicitTy, + sym.stmt.kind.expr.loc, + ); + } + return exprTy; + } + if (sym.tag === "FnParam") { + if (!node.kind.ty) { + this.cx.error(node.loc, `parameter must have a type`); + this.cx.fail(); + } + return this.cx.ty(node.kind.ty); + } + + throw new Error(`'${sym.tag}' not handled`); + } +} + +class PlaceChecker { + constructor( + private cx: CheckerCx, + ) {} checkPlace(node: ast.Node): Ty { if (node.is("UnaryExpr")) { @@ -89,285 +188,140 @@ class Checker { } } } - return this.checkExpr(node); + return this.cx.expr(node); } +} + +class ExprChecker { + constructor( + private cx: CheckerCx, + ) {} checkExpr(node: ast.Node): Ty { - const k = node.kind; - - if (node.is("Param")) { - const sym = this.syms.get(node); - - if (sym.tag === "Let") { - const exprTy = this.tys.expr(sym.stmt.kind.expr); - if (node.kind.ty) { - const explicitTy = this.tys.expr(node.kind.ty); - this.assertCompatible( - exprTy, - explicitTy, - sym.stmt.kind.expr.loc, - ); - } - return exprTy; - } - if (sym.tag === "FnParam") { - if (!node.kind.ty) { - this.error(node.loc, `parameter must have a type`); - this.fail(); - } - return this.tys.expr(node.kind.ty); - } - - throw new Error(`'${sym.tag}' not handled`); + const tag = node.kind.tag; + switch (tag) { + case "IdentExpr": + return this.checkIdentExpr(node.as(tag)); + case "IntExpr": + return this.checkIntExpr(node.as(tag)); + case "StrExpr": + return Ty.create("Ptr", { ty: Ty.Str }); + case "ArrayExpr": + return this.checkArrayExpr(node.as(tag)); + case "IndexExpr": + return this.checkIndexExpr(node.as(tag)); + case "CallExpr": + return this.checkCallExpr(node.as(tag)); + case "UnaryExpr": + return this.checkUnaryExpr(node.as(tag)); + case "BinaryExpr": + return this.checkBinaryExpr(node.as(tag)); + case "RangeExpr": + return this.checkRangeExpr(node.as(tag)); + default: + throw new Error(`'${node.kind.tag}' not unhandled`); } - - if (node.is("IdentExpr")) { - const sym = this.syms.get(node); - if (sym.tag === "Fn") { - return this.tys.fnStmt(sym.stmt); - } - if (sym.tag === "Bool") { - return Ty.Bool; - } - if (sym.tag === "Builtin") { - this.error(node.loc, `invalid use of builtin '${sym.id}'`); - this.fail(); - } - if (sym.tag === "FnParam") { - return this.tys.expr(sym.param); - } - if (sym.tag === "Let") { - return this.tys.expr(sym.param); - } - throw new Error(`'${sym.tag}' not handled`); - } - - if (node.is("IntExpr")) { - return Ty.Int; - } - - if (node.is("StrExpr")) { - return Ty.create("Ptr", { ty: Ty.Str }); - } - - if (node.is("ArrayExpr")) { - let ty: Ty | null = null; - for (const value of node.kind.values) { - const valueTy = this.tys.expr(value); - if (ty) { - this.assertCompatible(ty, valueTy, value.loc); - } else { - ty = valueTy; - } - } - if (!ty) { - this.error(node.loc, `could not infer type of empty array`); - this.fail(); - } - const length = node.kind.values.length; - return Ty.create("Array", { ty, length }); - } - - if (node.is("IndexExpr")) { - const exprTy = this.tys.place(node.kind.value); - const argTy = this.tys.expr(node.kind.arg); - if ( - (exprTy.is("Array") || exprTy.is("Slice")) && - argTy.compatibleWith(Ty.Int) - ) { - return exprTy.kind.ty; - } - if ( - (exprTy.is("Array") || exprTy.is("Slice")) && - argTy.compatibleWith(Ty.create("Range", {})) - ) { - return Ty.create("Slice", { ty: exprTy.kind.ty }); - } - if ( - exprTy.is("Str") && - argTy.compatibleWith(Ty.Int) - ) { - return Ty.Int; - } - this.error( - node.loc, - `cannot use index operator on '${exprTy.pretty()}' with '${argTy.pretty()}'`, - ); - this.fail(); - } - - if (node.is("CallExpr")) { - return this.checkCall(node); - } - - if (node.is("UnaryExpr")) { - const exprTy = this.tys.expr(node.kind.expr); - if (node.kind.op === "Negate" && exprTy.compatibleWith(Ty.Int)) { - return Ty.Int; - } - if (node.kind.op === "Not" && exprTy.compatibleWith(Ty.Bool)) { - return Ty.Bool; - } - if (node.kind.op === "Ref") { - return Ty.create("Ptr", { ty: exprTy }); - } - if (node.kind.op === "RefMut") { - return Ty.create("PtrMut", { ty: exprTy }); - } - if (node.kind.op === "Deref") { - if (exprTy.is("Ptr") || exprTy.is("PtrMut")) { - if (!exprTy.kind.ty.isSized()) { - this.error( - node.loc, - `cannot dereference unsized type '${exprTy.kind.ty.pretty()}' in an expression`, - ); - this.fail(); - } - return exprTy.kind.ty; - } - } - this.error( - node.loc, - `operator '${node.kind.tok}' cannot be applied to type '${exprTy.pretty()}'`, - ); - this.fail(); - } - - if (node.is("BinaryExpr")) { - const left = this.tys.expr(node.kind.left); - const right = this.tys.expr(node.kind.right); - const binaryOp = binaryOpPatterns - .find((pat) => - pat.op === node.kind.op && - left.compatibleWith(pat.left) && - right.compatibleWith(pat.right) - ); - if (!binaryOp) { - this.error( - node.loc, - `operator '${node.kind.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`, - ); - this.fail(); - } - return binaryOp.result; - } - - if (node.is("RangeExpr")) { - for (const operandExpr of [node.kind.begin, node.kind.end]) { - const operandTy = operandExpr && this.tys.expr(operandExpr); - if (operandTy && !operandTy.compatibleWith(Ty.Int)) { - this.error( - operandExpr.loc, - `range operand must be '${Ty.Int.pretty()}', not '${operandTy.pretty()}'`, - ); - this.fail(); - } - } - return Ty.create("Range", {}); - } - - if (node.is("IdentTy")) { - const sym = this.syms.get(node); - if (sym.tag === "BuiltinTy") { - switch (sym.ident) { - case "void": - return Ty.Void; - case "int": - return Ty.Int; - case "bool": - return Ty.Bool; - case "str": - return Ty.Str; - case "u8": - return Ty.U8; - default: - throw new Error( - `unknown type '${node.kind.ident}'`, - ); - } - } - this.error(node.loc, `symbol is not a type`); - this.fail(); - } - - if (node.is("PtrTy")) { - const ty = this.tys.expr(node.kind.ty); - return Ty.create("Ptr", { ty }); - } - if (node.is("PtrMutTy")) { - const ty = this.tys.expr(node.kind.ty); - return Ty.create("PtrMut", { ty }); - } - - if (node.is("ArrayTy")) { - const ty = this.tys.expr(node.kind.ty); - const lengthTy = this.tys.expr(node.kind.length); - if (!lengthTy.compatibleWith(Ty.Int)) { - this.error( - node.kind.length.loc, - `for array length, expected 'int', got '${lengthTy.pretty()}'`, - ); - this.fail(); - } - if (!node.kind.length.is("IntExpr")) { - this.error( - node.kind.length.loc, - `array length must be an 'int' expression`, - ); - this.fail(); - } - const length = node.kind.length.kind.value; - return Ty.create("Array", { ty, length }); - } - - if (node.is("SliceTy")) { - const ty = this.tys.expr(node.kind.ty); - return Ty.create("Slice", { ty }); - } - - throw new Error(`'${k.tag}' not unhandled`); } - private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty { + private checkIdentExpr(node: ast.NodeWithKind<"IdentExpr">): Ty { + const sym = this.cx.sym(node); + if (sym.tag === "Fn") { + return this.cx.fnStmt(sym.stmt); + } + if (sym.tag === "Bool") { + return Ty.Bool; + } + if (sym.tag === "Builtin") { + this.cx.error(node.loc, `invalid use of builtin '${sym.id}'`); + this.cx.fail(); + } + if (sym.tag === "FnParam") { + return this.cx.expr(sym.param); + } + if (sym.tag === "Let") { + return this.cx.expr(sym.param); + } + throw new Error(`'${sym.tag}' not handled`); + } + + private checkIntExpr(node: ast.NodeWithKind<"IntExpr">): Ty { + switch (node.kind.intTy) { + case "u8": + return Ty.U8; + case "u16": + return Ty.U16; + case "u32": + return Ty.U32; + case "u64": + return Ty.U64; + case "i8": + return Ty.U8; + case "i16": + return Ty.U16; + case "i32": + return Ty.I32; + case "i64": + return Ty.U64; + default: + throw new Error(`intType '${node.kind.intTy}' not handled`); + } + } + + private checkArrayExpr(node: ast.NodeWithKind<"ArrayExpr">): Ty { + let ty: Ty | null = null; + for (const value of node.kind.values) { + const valueTy = this.cx.expr(value); + if (ty) { + this.cx.assertCompatible(ty, valueTy, value.loc); + } else { + ty = valueTy; + } + } + if (!ty) { + this.cx.error(node.loc, `could not infer type of empty array`); + this.cx.fail(); + } + const length = node.kind.values.length; + return Ty.create("Array", { ty, length }); + } + + private checkIndexExpr(node: ast.NodeWithKind<"IndexExpr">): Ty { + const exprTy = this.cx.place(node.kind.value); + const argTy = this.cx.expr(node.kind.arg); + if ( + (exprTy.is("Array") || exprTy.is("Slice")) && + argTy.compatibleWith(Ty.I32) + ) { + return exprTy.kind.ty; + } + if ( + (exprTy.is("Array") || exprTy.is("Slice")) && + argTy.compatibleWith(Ty.create("Range", {})) + ) { + return Ty.create("Slice", { ty: exprTy.kind.ty }); + } + if ( + exprTy.is("Str") && + argTy.compatibleWith(Ty.I32) + ) { + return Ty.I32; + } + this.cx.error( + node.loc, + `cannot use index operator on '${exprTy.pretty()}' with '${argTy.pretty()}'`, + ); + this.cx.fail(); + } + + private checkCallExpr(node: ast.NodeWithKind<"CallExpr">): Ty { if (node.kind.value.is("IdentExpr")) { - const sym = this.syms.get(node.kind.value); + const sym = this.cx.sym(node.kind.value); if (sym.tag === "Builtin") { - if (sym.id === "len") { - if (node.kind.args.length !== 1) { - this.reportArgsIncorrectAmount( - node, - node.kind.args.length, - 0, - null, - ); - } - const argTy = this.tys.expr(node.kind.args[0]); - if ( - !(argTy.is("Array") || - argTy.is("Ptr") && - (argTy.kind.ty.is("Array") || - argTy.kind.ty.is("Slice") || - argTy.kind.ty.is("Str"))) - ) { - this.reportArgTypeNotCompatible( - node, - [argTy], - [Ty.Error], - null, - 0, - ); - } - return Ty.Int; - } - if (sym.id === "print") { - void node.kind.args - .map((arg) => this.tys.expr(arg)); - return Ty.Void; - } + return this.checkCallExprBuiltin(node, sym); } } - const calleeTy = this.tys.expr(node.kind.value); + const calleeTy = this.cx.expr(node.kind.value); const callableTy = calleeTy.is("Fn") ? calleeTy @@ -376,15 +330,15 @@ class Checker { : null; if (!callableTy) { - this.error( + this.cx.error( node.loc, `type '${calleeTy.pretty()}' not callable`, ); - this.fail(); + this.cx.fail(); } const args = node.kind.args - .map((arg) => this.tys.expr(arg)); + .map((arg) => this.cx.expr(arg)); const params = callableTy.kind.params; if (args.length !== params.length) { this.reportArgsIncorrectAmount( @@ -408,23 +362,132 @@ class Checker { return callableTy.kind.retTy; } + private checkCallExprBuiltin( + node: ast.NodeWithKind<"CallExpr">, + sym: Sym, + ): Ty { + if (!node.kind.value.is("IdentExpr")) { + throw new Error(); + } + if (sym.tag !== "Builtin") { + throw new Error(); + } + if (sym.id === "len") { + if (node.kind.args.length !== 1) { + this.reportArgsIncorrectAmount( + node, + node.kind.args.length, + 0, + null, + ); + } + const argTy = this.cx.expr(node.kind.args[0]); + if ( + !(argTy.is("Array") || + argTy.is("Ptr") && + (argTy.kind.ty.is("Array") || + argTy.kind.ty.is("Slice") || + argTy.kind.ty.is("Str"))) + ) { + this.reportArgTypeNotCompatible( + node, + [argTy], + [Ty.Error], + null, + 0, + ); + } + return Ty.I32; + } + if (sym.id === "print") { + void node.kind.args + .map((arg) => this.cx.expr(arg)); + return Ty.Void; + } + throw new Error(`builtin '${sym.id}' not handled`); + } + + private checkUnaryExpr(node: ast.NodeWithKind<"UnaryExpr">): Ty { + const exprTy = this.cx.expr(node.kind.expr); + if (node.kind.op === "Negate" && exprTy.compatibleWith(Ty.I32)) { + return Ty.I32; + } + if (node.kind.op === "Not" && exprTy.compatibleWith(Ty.Bool)) { + return Ty.Bool; + } + if (node.kind.op === "Ref") { + return Ty.create("Ptr", { ty: exprTy }); + } + if (node.kind.op === "RefMut") { + return Ty.create("PtrMut", { ty: exprTy }); + } + if (node.kind.op === "Deref") { + if (exprTy.is("Ptr") || exprTy.is("PtrMut")) { + if (!exprTy.kind.ty.isSized()) { + this.cx.error( + node.loc, + `cannot dereference unsized type '${exprTy.kind.ty.pretty()}' in an expression`, + ); + this.cx.fail(); + } + return exprTy.kind.ty; + } + } + this.cx.error( + node.loc, + `operator '${node.kind.tok}' cannot be applied to type '${exprTy.pretty()}'`, + ); + this.cx.fail(); + } + + private checkBinaryExpr(node: ast.NodeWithKind<"BinaryExpr">): Ty { + const left = this.cx.expr(node.kind.left); + const right = this.cx.expr(node.kind.right); + const result = binaryOpTests + .map((test) => test(node.kind.op, left, right)) + .filter((result) => result) + .at(0); + if (!result) { + this.cx.error( + node.loc, + `operator '${node.kind.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`, + ); + this.cx.fail(); + } + return result; + } + + private checkRangeExpr(node: ast.NodeWithKind<"RangeExpr">): Ty { + for (const operandExpr of [node.kind.begin, node.kind.end]) { + const operandTy = operandExpr && this.cx.expr(operandExpr); + if (operandTy && !operandTy.compatibleWith(Ty.I32)) { + this.cx.error( + operandExpr.loc, + `range operand must be '${Ty.I32.pretty()}', not '${operandTy.pretty()}'`, + ); + this.cx.fail(); + } + } + return Ty.create("Range", {}); + } + private reportArgsIncorrectAmount( node: ast.NodeWithKind<"CallExpr">, argsLength: number, paramsLength: number, calleeTy: Ty | null, ): never { - this.error( + this.cx.error( node.loc, `incorrect amount of arguments. got ${argsLength} expected ${paramsLength}`, ); if (calleeTy?.is("FnStmt")) { - this.info( + this.cx.info( calleeTy.kind.stmt.loc, "function defined here", ); } - this.fail(); + this.cx.fail(); } private reportArgTypeNotCompatible( @@ -434,14 +497,14 @@ class Checker { calleeTy: Ty | null, i: number, ): never { - this.error( + this.cx.error( node.kind.args[i].loc, `type '${args[i].pretty()}' not compatible with type '${ params[i].pretty() }', for argument ${i}`, ); if (calleeTy?.is("FnStmt")) { - this.info( + this.cx.info( calleeTy.kind.stmt.kind.params[i].loc, `parameter '${ calleeTy.kind.stmt.kind.params[i] @@ -449,50 +512,114 @@ class Checker { }' defined here`, ); } - this.fail(); - } - - private assertCompatible(left: Ty, right: Ty, loc: Loc): void { - if (!left.compatibleWith(right)) { - this.error( - loc, - `type '${left.pretty()}' not compatible with type '${right.pretty()}'`, - ); - this.fail(); - } - } - - private error(loc: Loc, message: string) { - this.reporter.error(loc, message); - } - - private info(loc: Loc, message: string) { - this.reporter.info(loc, message); - } - - private fail(): never { - this.reporter.abort(); + this.cx.fail(); } } -type BinaryOpPattern = { - op: ast.BinaryOp; - left: Ty; - right: Ty; - result: Ty; -}; +class TyChecker { + constructor( + private cx: CheckerCx, + ) {} -const binaryOpPatterns: BinaryOpPattern[] = [ - { op: "Add", left: Ty.Int, right: Ty.Int, result: Ty.Int }, - { op: "Subtract", left: Ty.Int, right: Ty.Int, result: Ty.Int }, - { op: "Multiply", left: Ty.Int, right: Ty.Int, result: Ty.Int }, - { op: "Divide", left: Ty.Int, right: Ty.Int, result: Ty.Int }, - { op: "Remainder", left: Ty.Int, right: Ty.Int, result: Ty.Int }, + checkTy(node: ast.Node): Ty { + if (node.is("IdentTy")) { + const sym = this.cx.sym(node); + if (sym.tag === "BuiltinTy") { + switch (sym.ident) { + case "void": + return Ty.Void; + case "bool": + return Ty.Bool; + case "str": + return Ty.Str; + 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 '${node.kind.ident}'`, + ); + } + } + this.cx.error(node.loc, `symbol is not a type`); + this.cx.fail(); + } - { op: "Eq", left: Ty.Int, right: Ty.Int, result: Ty.Bool }, - { op: "Ne", left: Ty.Int, right: Ty.Int, result: Ty.Bool }, - { op: "Lt", left: Ty.Int, right: Ty.Int, result: Ty.Bool }, - { op: "Gt", left: Ty.Int, right: Ty.Int, result: Ty.Bool }, - { op: "Lte", left: Ty.Int, right: Ty.Int, result: Ty.Bool }, - { op: "Gte", left: Ty.Int, right: Ty.Int, result: Ty.Bool }, + if (node.is("PtrTy")) { + const ty = this.cx.ty(node.kind.ty); + return Ty.create("Ptr", { ty }); + } + if (node.is("PtrMutTy")) { + const ty = this.cx.ty(node.kind.ty); + return Ty.create("PtrMut", { ty }); + } + + if (node.is("ArrayTy")) { + const ty = this.cx.ty(node.kind.ty); + const lengthTy = this.cx.expr(node.kind.length); + if (!lengthTy.compatibleWith(Ty.I32)) { + this.cx.error( + node.kind.length.loc, + `for array length, expected 'int', got '${lengthTy.pretty()}'`, + ); + this.cx.fail(); + } + if (!node.kind.length.is("IntExpr")) { + this.cx.error( + node.kind.length.loc, + `array length must be an 'int' expression`, + ); + this.cx.fail(); + } + const length = node.kind.length.kind.value; + return Ty.create("Array", { ty, length }); + } + + if (node.is("SliceTy")) { + const ty = this.cx.ty(node.kind.ty); + return Ty.create("Slice", { ty }); + } + + throw new Error(`'${node.kind.tag}' not unhandled`); + } +} + +type BinaryOpTest = (op: ast.BinaryOp, left: Ty, right: Ty) => Ty | null; + +const binaryOpTests: BinaryOpTest[] = [ + (op, left, right) => { + const ops = ["Add", "Subtract", "Multiply", "Divide", "Remainder"]; + if ( + ops.includes(op) && left.is("Int") && left.compatibleWith(right) + ) { + return left; + } + return null; + }, + (op, left, right) => { + const ops = ["Eq", "Ne", "Lt", "Gt", "Lte", "Gte"]; + if ( + ops.includes(op) && left.is("Int") && left.compatibleWith(right) + ) { + return Ty.Bool; + } + return null; + }, ]; diff --git a/src/front/parse.ts b/src/front/parse.ts index 9403071..4267f47 100644 --- a/src/front/parse.ts +++ b/src/front/parse.ts @@ -286,9 +286,28 @@ export class Parser { this.step(); return ast.Node.create(loc, "IdentExpr", { ident }); } else if (this.test("int")) { - const value = Number(this.current.value); + const match = this.current.value + .match(/(0|(?:[1-9][0-9]*))([iu](?:8|16|32|64|size))?$/); + if (!match) { + throw new Error(); + } + const value = Number(match[1]); + const intTy = match[2] ?? "i32"; + if ( + intTy && + !["8", "16", "32", "64", "size"].includes(intTy.slice(1)) + ) { + this.reporter.error( + loc, + `invalid integer size '${intTy[1]}'`, + ); + this.reporter.abort(); + } this.step(); - return ast.Node.create(loc, "IntExpr", { value }); + return ast.Node.create(loc, "IntExpr", { + value, + intTy: intTy as ast.IntTy ?? "i32", + }); } else if (this.test("str")) { const value = this.current.value; this.step(); @@ -415,9 +434,12 @@ export function tokenize(text: string, reporter: FileReporter): Tok[] { const type = keywordPattern.test(value) ? value : "ident"; return { type, value, loc }; }) - .add(/0|(?:[1-9][0-9]*)/, (loc, value) => { - return { type: "int", value, loc }; - }) + .add( + /(?:0|(?:[1-9][0-9]*))(?:[iu](?:8|16|32|64|size))?/, + (loc, value) => { + return { type: "int", value, loc }; + }, + ) .add(/"(?:[^\\"]|\\.)*"/, (loc, literal) => { let i = 1; let value = ""; diff --git a/src/front/resolve.ts b/src/front/resolve.ts index ceb4be2..f9cc4bd 100644 --- a/src/front/resolve.ts +++ b/src/front/resolve.ts @@ -160,7 +160,21 @@ class ResolverSyms { } resolveTy(ident: string): Sym | null { - const builtins: string[] = ["void", "int", "bool", "str", "u8"]; + const builtins: string[] = [ + "void", + "bool", + "str", + "i8", + "i16", + "i32", + "i64", + "isize", + "u8", + "u16", + "u32", + "u64", + "usize", + ]; if (builtins.includes(ident)) { return { tag: "BuiltinTy", ident } as Sym; } diff --git a/src/middle.ts b/src/middle.ts index bfeb295..d17c359 100644 --- a/src/middle.ts +++ b/src/middle.ts @@ -172,7 +172,7 @@ class FnLowerer { } private lowerLetStmt(stmt: ast.NodeWithKind<"LetStmt">) { - const ty = this.tys.expr(stmt.kind.param); + const ty = this.tys.param(stmt.kind.param.as("Param")); const expr = this.lowerExpr(stmt.kind.expr); const local = new Inst( Ty.create("PtrMut", { ty }), @@ -253,7 +253,7 @@ class FnLowerer { if (argTy.is("Int")) { const argInst = this.lowerExpr(arg); return this.pushInst( - Ty.create("Ptr", { ty: Ty.Int }), + Ty.create("Ptr", { ty: Ty.I32 }), "GetElemPtr", { base: valueInst, offset: argInst }, ); @@ -292,7 +292,10 @@ class FnLowerer { throw new Error(`'${sym.tag}' not handled`); } if (expr.is("IntExpr")) { - return this.pushInst(ty, "Int", { value: expr.kind.value }); + return this.pushInst(ty, "Int", { + value: expr.kind.value, + intTy: expr.kind.intTy, + }); } if (expr.is("StrExpr")) { return this.pushInst(ty, "Str", { value: expr.kind.value }); @@ -333,13 +336,10 @@ class FnLowerer { if (expr.is("BinaryExpr")) { const leftTy = this.tys.expr(expr.kind.left); const rightTy = this.tys.expr(expr.kind.right); - const binaryOp = binaryOpPatterns - .find((pat) => - expr.kind.op === pat.op && - ty.compatibleWith(pat.result) && - leftTy.compatibleWith(pat.left ?? pat.result) && - rightTy.compatibleWith(pat.right ?? pat.left ?? pat.result) - ); + const binaryOp = binaryOpTests + .map((test) => test(expr.kind.op, leftTy, rightTy, ty)) + .filter((tested) => tested) + .at(0); if (!binaryOp) { throw new Error( `'${expr.kind.op}' with '${ty.pretty()}' not handled`, @@ -347,7 +347,7 @@ class FnLowerer { } const left = this.lowerExpr(expr.kind.left); const right = this.lowerExpr(expr.kind.right); - return this.pushInst(ty, binaryOp.tag, { left, right }); + return this.pushInst(ty, binaryOp, { left, right }); } throw new Error(`'${expr.kind.tag}' not handled`); } @@ -357,11 +357,11 @@ class FnLowerer { const operandTy = this.tys.expr(expr.kind.expr); if ( expr.kind.op === "Negate" && - operandTy.compatibleWith(Ty.Int) && - resultTy.compatibleWith(Ty.Int) + operandTy.compatibleWith(Ty.I32) && + resultTy.compatibleWith(Ty.I32) ) { const operand = this.lowerExpr(expr.kind.expr); - return this.pushInst(Ty.Int, "Negate", { source: operand }); + return this.pushInst(Ty.I32, "Negate", { source: operand }); } if ( expr.kind.op === "Not" && @@ -426,25 +426,43 @@ class FnLowerer { } } -type BinaryOpPattern = { - op: ast.BinaryOp; - tag: BinaryOp; - result: Ty; - left?: Ty; - right?: Ty; -}; +type BinaryOpTest = ( + op: ast.BinaryOp, + left: Ty, + right: Ty, + result: Ty, +) => BinaryOp | null; -const binaryOpPatterns: BinaryOpPattern[] = [ - { op: "Add", tag: "Add", result: Ty.Int, left: Ty.Int }, - { op: "Subtract", tag: "Sub", result: Ty.Int, left: Ty.Int }, - { op: "Multiply", tag: "Mul", result: Ty.Int, left: Ty.Int }, - { op: "Divide", tag: "Div", result: Ty.Int, left: Ty.Int }, - { op: "Remainder", tag: "Rem", result: Ty.Int }, - - { op: "Eq", tag: "Eq", result: Ty.Bool, left: Ty.Int }, - { op: "Ne", tag: "Ne", result: Ty.Bool, left: Ty.Int }, - { op: "Lt", tag: "Lt", result: Ty.Bool, left: Ty.Int }, - { op: "Gt", tag: "Gt", result: Ty.Bool, left: Ty.Int }, - { op: "Lte", tag: "Lte", result: Ty.Bool, left: Ty.Int }, - { op: "Gte", tag: "Gte", result: Ty.Bool, left: Ty.Int }, +const binaryOpTests: BinaryOpTest[] = [ + (op, left, right, result) => { + const ops = ["Add", "Subtract", "Multiply", "Divide", "Remainder"]; + const tags: Record = { + "Add": "Add", + "Subtract": "Sub", + "Multiply": "Mul", + "Divide": "Div", + "Remainder": "Rem", + }; + if ( + ops.includes(op) && + left.is("Int") && + left.compatibleWith(right) && + result.compatibleWith(left) + ) { + return tags[op]; + } + return null; + }, + (op, left, right, result) => { + const ops = ["Eq", "Ne", "Lt", "Gt", "Lte", "Gte"]; + if ( + ops.includes(op) && + left.is("Int") && + left.compatibleWith(right) && + result.is("Bool") + ) { + return op as BinaryOp; + } + return null; + }, ]; diff --git a/src/mir.ts b/src/mir.ts index c97d8bd..5b498ec 100644 --- a/src/mir.ts +++ b/src/mir.ts @@ -119,6 +119,7 @@ export class Inst { case "Void": return ""; case "Int": + return `${k.value}${k.intTy}`; case "Bool": return `${k.value}`; case "Str": @@ -183,7 +184,7 @@ export class Inst { export type InstKind = | { tag: "Error" } | { tag: "Void" } - | { tag: "Int"; value: number } + | { tag: "Int"; value: number; intTy: ast.IntTy } | { tag: "Bool"; value: boolean } | { tag: "Str"; value: string } | { tag: "Array"; values: Inst[] } diff --git a/src/mir_interpreter.ts b/src/mir_interpreter.ts index 98bb3a7..7e53d73 100644 --- a/src/mir_interpreter.ts +++ b/src/mir_interpreter.ts @@ -316,7 +316,35 @@ export class FnInterpreter { const r = right.kind.value; const value = (() => { - const Int = (value: number) => new Val({ tag: "Int", value }); + const Int = (value: number) => + new Val({ + tag: "Int", + value: ((value: number) => { + if (!inst.ty.is("Int")) { + throw new Error(); + } + switch (inst.ty.kind.intTy) { + case "i8": + return value & 0xff; + case "i16": + return value & 0xffff; + case "i32": + return value & 0xffffffff; + case "i64": + case "isize": + return value; + case "u8": + return value & 0xff; + case "u16": + return value & 0xffff; + case "u32": + return value & 0xffffffff; + case "u64": + case "usize": + return value; + } + })(value), + }); const Bool = (value: boolean) => new Val({ tag: "Bool", value }); diff --git a/src/ty.ts b/src/ty.ts index e26b36e..52c8f1f 100644 --- a/src/ty.ts +++ b/src/ty.ts @@ -23,9 +23,17 @@ export class Ty { static Error = Ty.create("Error", {}); static Void = Ty.create("Void", {}); - static Int = Ty.create("Int", {}); static IntLiteral = Ty.create("IntLiteral", {}); - static U8 = Ty.create("UInt", { size: 8 }); + static I8 = Ty.create("Int", { intTy: "i8" }); + static I16 = Ty.create("Int", { intTy: "i16" }); + static I32 = Ty.create("Int", { intTy: "i32" }); + static I64 = Ty.create("Int", { intTy: "i64" }); + static ISize = Ty.create("Int", { intTy: "isize" }); + static U8 = Ty.create("Int", { intTy: "u8" }); + static U16 = Ty.create("Int", { intTy: "u16" }); + static U32 = Ty.create("Int", { intTy: "u32" }); + static U64 = Ty.create("Int", { intTy: "u64" }); + static USize = Ty.create("Int", { intTy: "usize" }); static Bool = Ty.create("Bool", {}); static Str = Ty.create("Str", {}); @@ -56,13 +64,14 @@ export class Ty { if (this.is("Void")) { return other.is("Void"); } - if (this.is("Int")) { - return other.is("Int"); - } if (this.is("IntLiteral")) { // return other.is("Int") || other.is("UInt"); return false; } + if (this.is("Int")) { + return other.is("Int") && this.kind.intTy == other.kind.intTy; + } + if (this.is("Bool")) { return other.is("Bool"); } @@ -125,15 +134,13 @@ export class Ty { } // redundant; sanity check if (this.kind.stmt.id !== other.kind.stmt.id) { - return false; + throw new Error(); } return true; } throw new Error(`'${this.kind.tag}' not handled`); } - // convertibleTo(other: Ty): boolean {} - isSized(): boolean { if (this.is("Slice") || this.is("Str")) { return false; @@ -147,12 +154,10 @@ export class Ty { return ""; case "Void": return "void"; - case "Int": - return "int"; case "IntLiteral": return "{integer}"; - case "UInt": - return `u${this.kind.size}`; + case "Int": + return `${this.kind.intTy}`; case "Bool": return "bool"; case "Str": @@ -191,9 +196,8 @@ export class Ty { export type TyKind = | { tag: "Error" } | { tag: "Void" } - | { tag: "Int" } | { tag: "IntLiteral" } - | { tag: "UInt"; size: IntSize } + | { tag: "Int"; intTy: ast.IntTy } | { tag: "Bool" } | { tag: "Str" } | { tag: "Ptr"; ty: Ty } @@ -203,5 +207,3 @@ export type TyKind = | { tag: "Range" } | { tag: "Fn"; params: Ty[]; retTy: Ty } | { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> }; - -export type IntSize = 8 | 16 | 32 | 64; diff --git a/tests/array.ethlang b/tests/array.ethlang index 6ea101d..5d07c83 100644 --- a/tests/array.ethlang +++ b/tests/array.ethlang @@ -1,21 +1,21 @@ fn main() { - let array: [int; 3] = [1, 2, 3]; + let array: [i32; 3] = [1, 2, 3]; - let elem: int = array[0]; + let elem: i32 = array[0]; // expect: 1 print(elem); - let ptr_to_array: *[int; 3] = &array; + let ptr_to_array: *[i32; 3] = &array; // expect: 2 print(ptr_to_array.*[1]); - let slice: *[int] = &array[..]; + let slice: *[i32] = &array[..]; // expect: 3 print(slice.*[2]); - let slice_mut: *mut [int] = &mut array[1..3]; + let slice_mut: *mut [i32] = &mut array[1..3]; slice_mut.*[0] = 4; // expect: 4 print(array[1]); diff --git a/tests/assign.ethlang b/tests/assign.ethlang index 46b50f1..33a74d9 100644 --- a/tests/assign.ethlang +++ b/tests/assign.ethlang @@ -2,7 +2,7 @@ fn main() { - let v: int = 123; + let v: i32 = 123; v = 456; print(v); } diff --git a/tests/fn_int.ethlang b/tests/fn_int.ethlang index d1bdbb2..c0b8c78 100644 --- a/tests/fn_int.ethlang +++ b/tests/fn_int.ethlang @@ -1,5 +1,5 @@ -fn my_int_fn() -> int { +fn my_int_fn() -> i32 { return 123; } diff --git a/tests/int.ethlang b/tests/int.ethlang index cad4c9a..5f65a1a 100644 --- a/tests/int.ethlang +++ b/tests/int.ethlang @@ -1,7 +1,17 @@ fn main() { - // let a: u8 = 0; + let a: u8 = 255u8; + // expect: 255 + print(a); + + a = a + 1u8; + // expect: 0 + print(a); + + let b = 256; + // expect: 256 + print(b); } // vim: syntax=rust commentstring=//\ %s diff --git a/tests/let.ethlang b/tests/let.ethlang index c702037..8d348c7 100644 --- a/tests/let.ethlang +++ b/tests/let.ethlang @@ -2,7 +2,7 @@ fn main() { let a = 123; - let b: int = 321; + let b: i32 = 321; let c = b; } diff --git a/tests/pointer.ethlang b/tests/pointer.ethlang index 4a35124..3af8287 100644 --- a/tests/pointer.ethlang +++ b/tests/pointer.ethlang @@ -1,5 +1,5 @@ -fn change_to(place: *mut int, value: int) +fn change_to(place: *mut i32, value: i32) { *place = value; } @@ -7,7 +7,7 @@ fn change_to(place: *mut int, value: int) fn main() { let a = 1; - let b: *int = &a; + let b: *i32 = &a; // expect: 1 print(*b); @@ -15,7 +15,7 @@ fn main() // expect: 2 print(*b); - let c: *mut int = &mut a; + let c: *mut i32 = &mut a; *c = 3; // expect: 3 print(a);