diff --git a/program.ethlang b/program.ethlang index b43b843..a650c4a 100644 --- a/program.ethlang +++ b/program.ethlang @@ -1,13 +1,14 @@ fn add(a: int, b: int) -> int { - return __add(a, b); + return a + b; } fn main() { - let sum: void = add(2, 3); + let sum = add(2, 3); print_int(sum); + print_int(2 - 3); } diff --git a/src/ast.ts b/src/ast.ts index a6fdde5..4bd09da 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -80,6 +80,8 @@ export class Node { return visit(); case "CallExpr": return visit(k.expr, ...k.args); + case "BinaryExpr": + return visit(k.left, k.right); case "IdentTy": return visit(); } @@ -106,8 +108,29 @@ export type NodeKind = | { tag: "IdentExpr"; ident: string } | { tag: "IntExpr"; value: number } | { tag: "CallExpr"; expr: Node; args: Node[] } + | { tag: "BinaryExpr"; op: BinaryOp; left: Node; right: Node; tok: string } | { tag: "IdentTy"; ident: string }; +export type BinaryOp = + | "Or" + | "And" + | "Eq" + | "Ne" + | "Lt" + | "Gt" + | "Lte" + | "Gte" + | "BitOr" + | "BitXor" + | "BitAnd" + | "Shl" + | "Shr" + | "Add" + | "Subtract" + | "Multiply" + | "Divide" + | "Remainder"; + export interface Visitor { visit(node: Node): void | "break"; } diff --git a/src/front.ts b/src/front.ts index 68f0f7a..cef61b3 100644 --- a/src/front.ts +++ b/src/front.ts @@ -1,23 +1,6 @@ import * as ast from "./ast.ts"; import { Ty } from "./ty.ts"; -const rootSyms = [ - { - id: "print_int", - ty: Ty.create("Fn", { - params: [Ty.Int], - retTy: Ty.Void, - }), - }, - { - id: "__add", - ty: Ty.create("Fn", { - params: [Ty.Int, Ty.Int], - retTy: Ty.Int, - }), - }, -]; - export class Checker { private nodeTys = new Map(); @@ -76,7 +59,7 @@ export class Checker { return this.check(sym.stmt); } if (sym.tag === "Builtin") { - return rootSyms.find((s) => s.id === sym.id)!.ty; + return builtins.find((s) => s.id === sym.id)!.ty; } if (sym.tag === "FnParam") { return this.check(sym.param); @@ -95,6 +78,25 @@ export class Checker { return this.checkCall(node); } + if (node.is("BinaryExpr")) { + const left = this.check(node.kind.left); + const right = this.check(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.line, + `operator '${node.kind.tok}' cannot be applied to types '${left.pretty()}' and '${right.pretty()}'`, + ); + this.fail(); + } + return binaryOp.result; + } + if (node.is("IdentTy")) { switch (node.kind.ident) { case "void": @@ -231,6 +233,18 @@ export class Checker { } } +type BinaryOpPattern = { + op: ast.BinaryOp; + left: Ty; + right: Ty; + result: Ty; +}; + +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 }, +]; + export type Sym = | { tag: "Error" } | { tag: "Builtin"; id: string } @@ -264,7 +278,7 @@ class ResolverSyms { static root(): ResolverSyms { return new ResolverSyms( new Map( - rootSyms.map<[string, Sym]>((sym) => [ + builtins.map<[string, Sym]>((sym) => [ sym.id, { tag: "Builtin", id: sym.id }, ]), @@ -365,6 +379,28 @@ export function resolve( return new ResolveMap(resols); } +type Builtin = { + id: string; + ty: Ty; +}; + +const builtins: Builtin[] = [ + { + id: "print_int", + ty: Ty.create("Fn", { + params: [Ty.Int], + retTy: Ty.Void, + }), + }, + { + id: "__add", + ty: Ty.create("Fn", { + params: [Ty.Int, Ty.Int], + retTy: Ty.Int, + }), + }, +]; + export function parse( filename: string, text: string, @@ -480,6 +516,57 @@ export class Parser { } parseExpr(): ast.Node { + return this.parseBinary(); + } + + parseBinary(prec = 7): ast.Node { + const loc = this.loc(); + if (prec == 0) { + return this.parsePrefixE(); + } + const ops: [Tok["type"], ast.BinaryOp, number][] = [ + ["or", "Or", 9], + ["and", "And", 8], + ["==", "Eq", 7], + ["!=", "Ne", 7], + ["<", "Lt", 7], + [">", "Gt", 7], + ["<=", "Lte", 7], + [">=", "Gte", 7], + ["|", "BitOr", 6], + ["^", "BitXor", 5], + ["&", "BitAnd", 4], + ["<<", "Shl", 3], + [">>", "Shr", 3], + ["+", "Add", 2], + ["-", "Subtract", 2], + ["*", "Multiply", 1], + ["/", "Divide", 1], + ["%", "Remainder", 1], + ]; + + let left = this.parseBinary(prec - 1); + + let should_continue = true; + while (should_continue) { + should_continue = false; + for (const [tok, op, p] of ops) { + if (prec >= p && this.eat(tok)) { + const right = this.parseBinary(prec - 1); + left = ast.Node.create( + loc, + "BinaryExpr", + { op, left, right, tok }, + ); + should_continue = true; + break; + } + } + } + return left; + } + + parsePrefixE() { return this.parsePostfix(); } @@ -588,13 +675,15 @@ export class Parser { export type Tok = { type: string; value: string; line: number }; -const keywordRegex = - /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)$/; +const keywordPattern = + /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)$/; +const operatorPattern = + /((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g; export function tokenize(text: string): Tok[] { return text .replace(/\/\/[^\n]*/g, "") - .replace(/((?:\-\>)|[\n\(\)\{\},.;:])/g, " $1 ") + .replace(operatorPattern, " $1 ") .split(/[ \t\r]/) .filter((value) => value !== "") .reduce<[[string, number][], number]>( @@ -612,7 +701,7 @@ export function tokenize(text: string): Tok[] { /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tok.value) ? { ...tok, - type: keywordRegex.test(tok.value) ? tok.value : "ident", + type: keywordPattern.test(tok.value) ? tok.value : "ident", } : tok ) diff --git a/src/main.ts b/src/main.ts index e00a6f7..9974db1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,7 +34,9 @@ if (!mainFn) { const m = new middle.MiddleLowerer(resols, checker); const mainMiddleFn = m.lowerFn(mainFn); -console.log(mainMiddleFn.pretty()); +if (!Deno.args.includes("--test")) { + console.log(mainMiddleFn.pretty()); +} const interp = new MirInterpreter(); interp.eval(mainMiddleFn); diff --git a/src/middle.ts b/src/middle.ts index 0bd8d24..186b83e 100644 --- a/src/middle.ts +++ b/src/middle.ts @@ -63,6 +63,12 @@ class FnLowerer { this.pushInst(Ty.Void, "Return", { source }); return; } + if (stmt.is("AssignStmt")) { + const source = this.lowerExpr(stmt.kind.expr); + const target = this.lowerAssignPlace(stmt.kind.place); + this.pushInst(Ty.Void, "LocalStore", { target, source }); + return; + } if (stmt.is("ExprStmt")) { this.lowerExpr(stmt.kind.expr); return; @@ -70,6 +76,21 @@ class FnLowerer { throw new Error(`'${stmt.kind.tag}' not handled`); } + private lowerAssignPlace(place: ast.Node): Inst { + if (place.is("IdentExpr")) { + const sym = this.resols.get(place); + if (sym.tag === "Let") { + const local = this.localMap.get(sym.param.id); + if (!local) { + throw new Error(); + } + return local; + } + throw new Error(`'${sym.tag}' not handled`); + } + throw new Error(`'${place.kind.tag}' not handled`); + } + private lowerExpr(expr: ast.Node): Inst { if (expr.is("IdentExpr")) { const sym = this.resols.get(expr); @@ -118,6 +139,19 @@ class FnLowerer { const callee = this.lowerExpr(expr.kind.expr); return this.pushInst(ty, "Call", { callee, args }); } + if (expr.is("BinaryExpr")) { + const ty = this.checker.check(expr); + const binaryOp = binaryOpPatterns + .find((pat) => + expr.kind.op === pat.op && ty.compatibleWith(pat.ty) + ); + if (!binaryOp) { + throw new Error(); + } + const left = this.lowerExpr(expr.kind.left); + const right = this.lowerExpr(expr.kind.right); + return this.pushInst(ty, binaryOp.tag, { left, right }); + } throw new Error(`'${expr.kind.tag}' not handled`); } @@ -138,6 +172,17 @@ class FnLowerer { } } +type BinaryOpPattern = { + op: ast.BinaryOp; + ty: Ty; + tag: BinaryOp; +}; + +const binaryOpPatterns: BinaryOpPattern[] = [ + { op: "Add", ty: Ty.Int, tag: "Add" }, + { op: "Subtract", ty: Ty.Int, tag: "Sub" }, +]; + export class Fn { constructor( public stmt: ast.FnStmt, @@ -237,7 +282,22 @@ export class Inst { return ` ${r(k.target)}, ${r(k.source)}`; case "Return": return ` ${r(k.source)}`; + case "Eq": + case "Ne": + case "Lt": + case "Gt": + case "Lte": + case "Gte": + case "BitOr": + case "BitXor": + case "BitAnd": + case "Shl": + case "Shr": case "Add": + case "Sub": + case "Mul": + case "Div": + case "Rem": return ` ${r(k.left)} ${r(k.right)}`; case "DebugPrint": return ` ${k.args.map(r).join(", ")}`; @@ -259,5 +319,23 @@ export type InstKind = | { tag: "LocalLoad"; source: Inst } | { tag: "LocalStore"; target: Inst; source: Inst } | { tag: "Return"; source: Inst } - | { tag: "Add"; left: Inst; right: Inst } + | { tag: BinaryOp; left: Inst; right: Inst } | { tag: "DebugPrint"; args: Inst[] }; + +export type BinaryOp = + | "Eq" + | "Ne" + | "Lt" + | "Gt" + | "Lte" + | "Gte" + | "BitOr" + | "BitXor" + | "BitAnd" + | "Shl" + | "Shr" + | "Add" + | "Sub" + | "Mul" + | "Div" + | "Rem"; diff --git a/src/mir_interpreter.ts b/src/mir_interpreter.ts index e049c4a..792ba8a 100644 --- a/src/mir_interpreter.ts +++ b/src/mir_interpreter.ts @@ -57,26 +57,63 @@ export class MirInterpreter { continue; case "Return": return regs.get(k.source)!; - case "Add": { + case "Eq": + case "Ne": + case "Lt": + case "Gt": + case "Lte": + case "Gte": + case "BitOr": + case "BitXor": + case "BitAnd": + case "Shl": + case "Shr": + case "Add": + case "Sub": + case "Mul": + case "Div": + case "Rem": { const left = regs.get(k.left)!; const right = regs.get(k.right)!; - if (left.kind.tag === "Int" && right.kind.tag == "Int") { - regs.set( - inst, - new Val({ - tag: "Int", - value: left.kind.value + right.kind.value, - }), - ); + const lk = left.kind; + const rk = right.kind; + + if (lk.tag === "Int" && rk.tag === "Int") { + const value = (() => { + switch (k.tag) { + case "Eq": + case "Ne": + case "Lt": + case "Gt": + case "Lte": + case "Gte": + case "BitOr": + case "BitXor": + case "BitAnd": + case "Shl": + case "Shr": + break; + case "Add": + return lk.value + rk.value; + case "Sub": + return lk.value - rk.value; + case "Mul": + case "Div": + case "Rem": + break; + } + throw new Error(`'${k.tag}' not handled`); + })(); + regs.set(inst, new Val({ tag: "Int", value })); continue; } - throw new Error(); + throw new Error(`'${k.tag}' not handled`); } case "DebugPrint": console.log( - `debug: ${ - k.args.map((a) => regs.get(a)!.pretty()).join(", ") - }`, + k.args + .map((a) => regs.get(a)!.pretty()) + .join(", "), ); continue; } diff --git a/tests/assign.ethlang b/tests/assign.ethlang new file mode 100644 index 0000000..1f61661 --- /dev/null +++ b/tests/assign.ethlang @@ -0,0 +1,9 @@ +// expect: 456 + +fn main() +{ + let v: int = 123; + v = 456; + print_int(v); +} + diff --git a/tests/fn_int.ethlang b/tests/fn_int.ethlang new file mode 100644 index 0000000..d1bdbb2 --- /dev/null +++ b/tests/fn_int.ethlang @@ -0,0 +1,10 @@ + +fn my_int_fn() -> int { + return 123; +} + +fn main() +{ + my_int_fn(); +} + diff --git a/tests/fn_void.ethlang b/tests/fn_void.ethlang new file mode 100644 index 0000000..7e32da5 --- /dev/null +++ b/tests/fn_void.ethlang @@ -0,0 +1,12 @@ + +fn my_implicit_void() {} + +fn my_explicit_void() -> void {} + +fn main() +{ + my_implicit_void(); + my_explicit_void(); +} + + diff --git a/tests/let.ethlang b/tests/let.ethlang new file mode 100644 index 0000000..c702037 --- /dev/null +++ b/tests/let.ethlang @@ -0,0 +1,8 @@ + +fn main() +{ + let a = 123; + let b: int = 321; + let c = b; +} + diff --git a/tests/operators.ethlang b/tests/operators.ethlang new file mode 100644 index 0000000..ed623bd --- /dev/null +++ b/tests/operators.ethlang @@ -0,0 +1,12 @@ +// expect: 8 +// expect: 2 + +fn main() +{ + let a = 5; + let b = 3; + + print_int(a + b); + print_int(a - b); +} + diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..24a0aa3 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -e + +TEST_DIR=$(dirname $0) +SRC_DIR=$TEST_DIR/../src + +TEST_SRC=$(fd '\.ethlang' $TEST_DIR) + +count_total=0 +count_succeeded=0 + +for test_file in $TEST_SRC +do + echo "- $(basename $test_file)" + set +e + output=$(deno run -A $SRC_DIR/main.ts $test_file --test) + status=$? + set -e + + + if [[ status -ne 0 ]] + then + echo "-- failed: exit code $status --" + fi + + if [[ status -eq 0 ]] + then + if grep -q '// expect:' $test_file + then + expected=$(grep '// expect:' $test_file | sed -E 's/\/\/ expect: (.*?)/\1/g') + if [[ $output != $expected ]] + then + echo "-- failed: incorrect output --" + echo "-- expected --" + echo "$expected" + echo "-- actual --" + echo "$output" + status=1 + fi + fi + fi + + count_total=$(($count_total + 1)) + if [[ status -eq 0 ]] + then + count_succeeded=$(($count_succeeded + 1)) + else + echo "failed" + fi + +done + +if [[ $count_succeeded -eq $count_total ]] +then + echo "=== all tests passed ($count_succeeded/$count_total passed) ===" +else + echo "=== tests failed ($count_succeeded/$count_total passed) ===" +fi + +