diff --git a/src/ast.ts b/src/ast.ts index 5de513c..c676eaa 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -94,6 +94,8 @@ export class Node { return visit(); case "IntExpr": return visit(); + case "StrExpr": + return visit(); case "ArrayExpr": return visit(...k.values); case "IndexExpr": @@ -141,6 +143,7 @@ export type NodeKind = | { tag: "Param"; ident: string; ty: Node | null } | { tag: "IdentExpr"; ident: string } | { tag: "IntExpr"; value: number } + | { tag: "StrExpr"; value: string } | { tag: "ArrayExpr"; values: Node[] } | { tag: "IndexExpr"; value: Node; arg: Node } | { tag: "CallExpr"; value: Node; args: Node[] } diff --git a/src/front/check.ts b/src/front/check.ts index cef3b06..ab1d864 100644 --- a/src/front/check.ts +++ b/src/front/check.ts @@ -146,6 +146,10 @@ class Checker { 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) { @@ -179,6 +183,12 @@ class Checker { ) { 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()}'`, @@ -264,6 +274,8 @@ class Checker { return Ty.Int; case "bool": return Ty.Bool; + case "str": + return Ty.Str; default: this.error(node.loc, `unknown type '${node.kind.ident}'`); } @@ -310,9 +322,36 @@ class Checker { private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty { if (node.kind.value.is("IdentExpr")) { const sym = this.syms.get(node.kind.value); - if (sym && sym.tag === "Builtin") { - if (sym.id === "debug_print") { - const _argTys = node.kind.args + 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; } @@ -339,41 +378,71 @@ class Checker { .map((arg) => this.tys.expr(arg)); const params = callableTy.kind.params; if (args.length !== params.length) { - this.error( - node.loc, - `incorrect amount of arguments. got ${args.length} expected ${params.length}`, + this.reportArgsIncorrectAmount( + node, + args.length, + params.length, + calleeTy, ); - if (calleeTy.is("FnStmt")) { - this.info( - calleeTy.kind.stmt.loc, - "function defined here", - ); - } - this.fail(); } for (const i of args.keys()) { if (!args[i].compatibleWith(params[i])) { - this.error( - node.kind.args[i].loc, - `type '${args[i].pretty()}' not compatible with type '${ - params[i].pretty() - }', for argument ${i}`, + this.reportArgTypeNotCompatible( + node, + args, + params, + calleeTy, + i, ); - if (calleeTy.is("FnStmt")) { - this.info( - calleeTy.kind.stmt.kind.params[i].loc, - `parameter '${ - calleeTy.kind.stmt.kind.params[i] - .as("Param").kind.ident - }' defined here`, - ); - } - this.fail(); } } return callableTy.kind.retTy; } + private reportArgsIncorrectAmount( + node: ast.NodeWithKind<"CallExpr">, + argsLength: number, + paramsLength: number, + calleeTy: Ty | null, + ): never { + this.error( + node.loc, + `incorrect amount of arguments. got ${argsLength} expected ${paramsLength}`, + ); + if (calleeTy?.is("FnStmt")) { + this.info( + calleeTy.kind.stmt.loc, + "function defined here", + ); + } + this.fail(); + } + + private reportArgTypeNotCompatible( + node: ast.NodeWithKind<"CallExpr">, + args: Ty[], + params: Ty[], + calleeTy: Ty | null, + i: number, + ): never { + this.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( + calleeTy.kind.stmt.kind.params[i].loc, + `parameter '${ + calleeTy.kind.stmt.kind.params[i] + .as("Param").kind.ident + }' defined here`, + ); + } + this.fail(); + } + private assertCompatible(left: Ty, right: Ty, loc: Loc): void { if (!left.compatibleWith(right)) { this.error( diff --git a/src/front/parse.ts b/src/front/parse.ts index d2896ae..9403071 100644 --- a/src/front/parse.ts +++ b/src/front/parse.ts @@ -289,6 +289,10 @@ export class Parser { const value = Number(this.current.value); this.step(); return ast.Node.create(loc, "IntExpr", { value }); + } else if (this.test("str")) { + const value = this.current.value; + this.step(); + return ast.Node.create(loc, "StrExpr", { value }); } else if (this.eat("(")) { const expr = this.parseExpr(); this.mustEat(")"); @@ -414,6 +418,25 @@ export function tokenize(text: string, reporter: FileReporter): Tok[] { .add(/0|(?:[1-9][0-9]*)/, (loc, value) => { return { type: "int", value, loc }; }) + .add(/"(?:[^\\"]|\\.)*"/, (loc, literal) => { + let i = 1; + let value = ""; + while (i < literal.length - 1) { + if (literal[i] === "\\") { + i += 1; + value += { + "0": "\0", + "t": "\t", + "r": "\r", + "n": "\n", + }[literal[i]] ?? literal[i]; + } else { + value += literal[i]; + } + i += 1; + } + return { type: "str", value, loc }; + }) .add(/./, (loc, value) => { const escapedChar = JSON.stringify(value[0]).slice(1, -1); reporter.error(loc, `illegal character '${escapedChar}'`); diff --git a/src/front/resolve.ts b/src/front/resolve.ts index c0d832b..931001b 100644 --- a/src/front/resolve.ts +++ b/src/front/resolve.ts @@ -113,7 +113,8 @@ class ResolverSyms { static root(): ResolverSyms { return new ResolverSyms( new Map([ - ["debug_print", { tag: "Builtin", id: "debug_print" }], + ["len", { tag: "Builtin", id: "len" }], + ["print", { tag: "Builtin", id: "print" }], ]), null, ); diff --git a/src/main.ts b/src/main.ts index ecbbb17..7904aae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -46,7 +46,8 @@ function printMirFn(fn: mir.Fn) { .split("\n") .map<[string, string[]]>((line) => [line, []]) .map<[string, string[]]>(([line, colors]) => { - line = line.replace(/%(\d+)/g, "__%$1__"); + line = line.replace(/%\d+/g, "__$&__"); + line = line.replace(/"(?:[^"\\]|\\.)*"/g, "__$&__"); if (/^\s*__%\d+__ \(.*?\) =/.test(line)) { line = line.replace( /__%(\d+)__ \((.*?)\) =/g, @@ -56,15 +57,22 @@ function printMirFn(fn: mir.Fn) { } if (/[A-Z][a-zA-Z]+/.test(line)) { line = line.replace(/([A-Z][a-zA-Z]+)/, "%c$1%c"); - colors.push("color: red;", ""); + colors.push("color: red; font-weight: bold;", ""); } if (/\bptr\b/.test(line)) { line = line.replace(/\b(ptr)\b/, "%c$1%c"); colors.push("color: gray;", ""); } - while (/__%\d+__/.test(line)) { - line = line.replace(/__%(\d+)__/, "%c%$1%c"); - colors.push("color: lightblue;", ""); + while (true) { + if (/__%\d+__/.test(line)) { + line = line.replace(/__%(\d+)__/, "%c%$1%c"); + colors.push("color: lightblue;", ""); + } else if (/__"(?:[^"\\]|\\.)*"__/.test(line)) { + line = line.replace(/__("(?:[^"\\]|\\.)*")__/, "%c$1%c"); + colors.push("color: green;", ""); + } else { + break; + } } return [line, colors]; diff --git a/src/middle.ts b/src/middle.ts index 42b22ac..bfeb295 100644 --- a/src/middle.ts +++ b/src/middle.ts @@ -221,33 +221,43 @@ class FnLowerer { const valueTy = this.tys.place(value); const arg = place.kind.arg; const argTy = this.tys.expr(arg); - if (!valueTy.is("Array") && !valueTy.is("Slice")) { - throw new Error(); - } - const valueInst = this.lowerPlace(place.kind.value); - if (argTy.is("Int")) { - const argInst = this.lowerExpr(arg); - return this.pushInst( - Ty.create("PtrMut", { ty: valueTy.kind.ty }), - "GetElemPtr", - { base: valueInst, offset: argInst }, - ); - } - if (argTy.is("Range")) { - if (!arg.is("RangeExpr")) { - throw new Error("not supported yet"); + if (valueTy.is("Array") || valueTy.is("Slice")) { + const valueInst = this.lowerPlace(place.kind.value); + if (argTy.is("Int")) { + const argInst = this.lowerExpr(arg); + return this.pushInst( + Ty.create("PtrMut", { ty: valueTy.kind.ty }), + "GetElemPtr", + { base: valueInst, offset: argInst }, + ); + } + if (argTy.is("Range")) { + if (!arg.is("RangeExpr")) { + throw new Error("not supported yet"); + } + const begin = arg.kind.begin && + this.lowerExpr(arg.kind.begin); + const end = arg.kind.end && + this.lowerExpr(arg.kind.end); + return this.pushInst( + Ty.create("PtrMut", { + ty: Ty.create("Slice", { ty: valueTy.kind.ty }), + }), + "Slice", + { value: valueInst, begin, end }, + ); + } + } + if (valueTy.is("Str")) { + const valueInst = this.lowerPlace(place.kind.value); + if (argTy.is("Int")) { + const argInst = this.lowerExpr(arg); + return this.pushInst( + Ty.create("Ptr", { ty: Ty.Int }), + "GetElemPtr", + { base: valueInst, offset: argInst }, + ); } - const begin = arg.kind.begin && - this.lowerExpr(arg.kind.begin); - const end = arg.kind.end && - this.lowerExpr(arg.kind.end); - return this.pushInst( - Ty.create("PtrMut", { - ty: Ty.create("Slice", { ty: valueTy.kind.ty }), - }), - "Slice", - { value: valueInst, begin, end }, - ); } throw new Error( `${place.kind.tag} with arg ${argTy.pretty()} not handled`, @@ -282,7 +292,10 @@ class FnLowerer { throw new Error(`'${sym.tag}' not handled`); } if (expr.is("IntExpr")) { - return this.pushInst(Ty.Int, "Int", { value: expr.kind.value }); + return this.pushInst(ty, "Int", { value: expr.kind.value }); + } + if (expr.is("StrExpr")) { + return this.pushInst(ty, "Str", { value: expr.kind.value }); } if (expr.is("ArrayExpr")) { const ty = this.tys.expr(expr); @@ -295,14 +308,16 @@ class FnLowerer { return this.pushInst(ty, "Load", { source }); } if (expr.is("CallExpr")) { - const ty = this.tys.expr(expr); const args = expr.kind.args .map((arg) => this.lowerExpr(arg)); if (expr.kind.value.is("IdentExpr")) { const sym = this.syms.get(expr.kind.value); if (sym.tag === "Builtin") { - if (sym.id === "debug_print") { + if (sym.id === "len") { + return this.pushInst(ty, "Len", { source: args[0] }); + } + if (sym.id === "print") { return this.pushInst(ty, "DebugPrint", { args }); } throw new Error(`builtin '${sym.id}' not handled`); @@ -316,24 +331,23 @@ class FnLowerer { return this.lowerUnaryExpr(expr); } if (expr.is("BinaryExpr")) { - const resultTy = this.tys.expr(expr); 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 && - resultTy.compatibleWith(pat.result) && + ty.compatibleWith(pat.result) && leftTy.compatibleWith(pat.left ?? pat.result) && rightTy.compatibleWith(pat.right ?? pat.left ?? pat.result) ); if (!binaryOp) { throw new Error( - `'${expr.kind.op}' with '${resultTy.pretty()}' not handled`, + `'${expr.kind.op}' with '${ty.pretty()}' not handled`, ); } const left = this.lowerExpr(expr.kind.left); const right = this.lowerExpr(expr.kind.right); - return this.pushInst(resultTy, binaryOp.tag, { left, right }); + return this.pushInst(ty, binaryOp.tag, { left, right }); } throw new Error(`'${expr.kind.tag}' not handled`); } diff --git a/src/mir.ts b/src/mir.ts index 63d193e..c97d8bd 100644 --- a/src/mir.ts +++ b/src/mir.ts @@ -121,6 +121,8 @@ export class Inst { case "Int": case "Bool": return `${k.value}`; + case "Str": + return `${JSON.stringify(k.value)}`; case "Array": return `[${k.values.map(r).join(", ")}]`; case "Fn": @@ -169,6 +171,8 @@ export class Inst { case "Div": case "Rem": return `${r(k.left)} ${r(k.right)}`; + case "Len": + return `${r(k.source)}`; case "DebugPrint": return `${k.args.map(r).join(", ")}`; } @@ -181,6 +185,7 @@ export type InstKind = | { tag: "Void" } | { tag: "Int"; value: number } | { tag: "Bool"; value: boolean } + | { tag: "Str"; value: string } | { tag: "Array"; values: Inst[] } | { tag: "Fn"; fn: Fn } | { tag: "Param"; idx: number } @@ -196,6 +201,7 @@ export type InstKind = | { tag: "Not"; source: Inst } | { tag: "Negate"; source: Inst } | { tag: BinaryOp; left: Inst; right: Inst } + | { tag: "Len"; source: Inst } | { tag: "DebugPrint"; args: Inst[] }; export type BinaryOp = diff --git a/src/mir_interpreter.ts b/src/mir_interpreter.ts index f54337c..98bb3a7 100644 --- a/src/mir_interpreter.ts +++ b/src/mir_interpreter.ts @@ -31,6 +31,7 @@ export class FnInterpreter { case "Void": case "Int": case "Bool": + case "Str": this.regs.set(inst, new Val(k)); break; case "Array": @@ -59,7 +60,6 @@ export class FnInterpreter { if (base.kind.tag === "Ptr") { const array = base.kind.value; if (array.kind.tag !== "Array") { - console.log({ array }); throw new Error(); } if ( @@ -91,6 +91,22 @@ export class FnInterpreter { mutable: base.kind.mutable, }), ); + } else if (base.kind.tag === "Str") { + const str = base.kind.value; + if ( + offset.kind.value < 0 || + offset.kind.value >= str.length + ) { + throw new Error(); + } + this.regs.set( + inst, + new Val({ + tag: "StrElemPtr", + value: str, + idx: offset.kind.value, + }), + ); } else { throw new Error(); } @@ -172,8 +188,17 @@ export class FnInterpreter { inst, source.kind.values[source.kind.idx], ); + } else if (source.kind.tag === "StrElemPtr") { + this.regs.set( + inst, + new Val({ + tag: "Int", + value: source.kind.value.charCodeAt( + source.kind.idx, + ), + }), + ); } else { - console.log({ source }); throw new Error(); } break; @@ -248,6 +273,23 @@ export class FnInterpreter { case "Rem": this.evalBinaryOp(inst, k); break; + case "Len": { + const source = this.regs.get(k.source)!; + if (source.kind.tag === "Array") { + throw new Error(); + } else if (source.kind.tag === "Str") { + this.regs.set( + inst, + new Val({ + tag: "Int", + value: source.kind.value.length, + }), + ); + } else { + throw new Error(); + } + break; + } case "DebugPrint": console.log( k.args @@ -335,8 +377,11 @@ class Val { case "Int": case "Bool": return `${k.value}`; + case "Str": + return k.value; case "Ptr": case "ArrayElemPtr": + case "StrElemPtr": return ``; case "ArraySlice": return `[${ @@ -361,6 +406,9 @@ type ValKind = | { tag: "Void" } | { tag: "Int"; value: number } | { tag: "Bool"; value: boolean } + | { tag: "Str"; value: string } + | { tag: "Array"; values: Val[] } + | { tag: "Fn"; fn: mir.Fn } | { tag: "Ptr"; mutable: boolean; value: Val } | { tag: "ArrayElemPtr"; mutable: boolean; values: Val[]; idx: number } | { @@ -370,5 +418,4 @@ type ValKind = begin: number; end: number; } - | { tag: "Array"; values: Val[] } - | { tag: "Fn"; fn: mir.Fn }; + | { tag: "StrElemPtr"; value: string; idx: number }; diff --git a/src/ty.ts b/src/ty.ts index 8f912d5..9505e53 100644 --- a/src/ty.ts +++ b/src/ty.ts @@ -25,6 +25,7 @@ export class Ty { static Void = Ty.create("Void", {}); static Int = Ty.create("Int", {}); static Bool = Ty.create("Bool", {}); + static Str = Ty.create("Str", {}); private internHash(): string { return JSON.stringify(this.kind); @@ -59,6 +60,9 @@ export class Ty { if (this.is("Bool")) { return other.is("Bool"); } + if (this.is("Str")) { + return other.is("Str"); + } if (this.is("Ptr")) { if (!other.is("Ptr")) { return false; @@ -123,7 +127,7 @@ export class Ty { } isSized(): boolean { - if (this.is("Slice")) { + if (this.is("Slice") || this.is("Str")) { return false; } return true; @@ -139,6 +143,8 @@ export class Ty { return "int"; case "Bool": return "bool"; + case "Str": + return "str"; case "Ptr": return `*${this.kind.ty.pretty()}`; case "PtrMut": @@ -175,6 +181,7 @@ export type TyKind = | { tag: "Void" } | { tag: "Int" } | { tag: "Bool" } + | { tag: "Str" } | { tag: "Ptr"; ty: Ty } | { tag: "PtrMut"; ty: Ty } | { tag: "Array"; ty: Ty; length: number } diff --git a/tests/array.ethlang b/tests/array.ethlang index e31510b..6ea101d 100644 --- a/tests/array.ethlang +++ b/tests/array.ethlang @@ -5,20 +5,20 @@ fn main() let elem: int = array[0]; // expect: 1 - debug_print(elem); + print(elem); let ptr_to_array: *[int; 3] = &array; // expect: 2 - debug_print(ptr_to_array.*[1]); + print(ptr_to_array.*[1]); let slice: *[int] = &array[..]; // expect: 3 - debug_print(slice.*[2]); + print(slice.*[2]); let slice_mut: *mut [int] = &mut array[1..3]; slice_mut.*[0] = 4; // expect: 4 - debug_print(array[1]); + print(array[1]); } // vim: syntax=rust commentstring=//\ %s diff --git a/tests/assign.ethlang b/tests/assign.ethlang index a5319b2..46b50f1 100644 --- a/tests/assign.ethlang +++ b/tests/assign.ethlang @@ -4,6 +4,6 @@ fn main() { let v: int = 123; v = 456; - debug_print(v); + print(v); } diff --git a/tests/if.ethlang b/tests/if.ethlang index e4d968c..3bbaf2a 100644 --- a/tests/if.ethlang +++ b/tests/if.ethlang @@ -13,7 +13,7 @@ fn main() a = 3; } - debug_print(a); + print(a); if false { a = 4; @@ -21,5 +21,5 @@ fn main() a = 5; } - debug_print(a); + print(a); } diff --git a/tests/loop.ethlang b/tests/loop.ethlang index 7b4e76e..240c00a 100644 --- a/tests/loop.ethlang +++ b/tests/loop.ethlang @@ -9,7 +9,7 @@ fn main() a = a + 1; } - debug_print(a); + print(a); while a < 10 { if a >= 8 { @@ -18,5 +18,5 @@ fn main() a = a + 1; } - debug_print(a); + print(a); } diff --git a/tests/operators.ethlang b/tests/operators.ethlang index 4a52f93..1208e7e 100644 --- a/tests/operators.ethlang +++ b/tests/operators.ethlang @@ -13,20 +13,20 @@ fn main() let a = 5; let b = 3; - debug_print(a + b); - debug_print(a - b); - debug_print(a * b); - debug_print(a * b / 2); - debug_print(a % b); - debug_print(-a); + print(a + b); + print(a - b); + print(a * b); + print(a * b / 2); + print(a % b); + print(-a); let c = false; if not c { - debug_print(123); + print(123); } - debug_print(2 * 3 + 4); - debug_print(2 * (3 + 4)); + print(2 * 3 + 4); + print(2 * (3 + 4)); } diff --git a/tests/pointer.ethlang b/tests/pointer.ethlang index 8cca2b1..4a35124 100644 --- a/tests/pointer.ethlang +++ b/tests/pointer.ethlang @@ -9,21 +9,21 @@ fn main() let a = 1; let b: *int = &a; // expect: 1 - debug_print(*b); + print(*b); a = 2; // expect: 2 - debug_print(*b); + print(*b); let c: *mut int = &mut a; *c = 3; // expect: 3 - debug_print(a); + print(a); // expect: 3 - debug_print(*c); + print(*c); change_to(&mut a, 4); // expect: 4 - debug_print(a); + print(a); } diff --git a/tests/string.ethlang b/tests/string.ethlang new file mode 100644 index 0000000..f58587e --- /dev/null +++ b/tests/string.ethlang @@ -0,0 +1,14 @@ + +fn main() +{ + let my_string: *str = "hello world"; + // expect: hello world + print(my_string); + // expect: 104 + print(my_string.*[0]); + // expect: 11 + print(len(my_string)); +} + +// vim: syntax=rust commentstring=//\ %s +