tests + binary ops

This commit is contained in:
sfja 2026-03-10 23:21:42 +01:00
parent 3b3a189020
commit bdee8b5bed
12 changed files with 382 additions and 40 deletions

View File

@ -1,13 +1,14 @@
fn add(a: int, b: int) -> int fn add(a: int, b: int) -> int
{ {
return __add(a, b); return a + b;
} }
fn main() fn main()
{ {
let sum: void = add(2, 3); let sum = add(2, 3);
print_int(sum); print_int(sum);
print_int(2 - 3);
} }

View File

@ -80,6 +80,8 @@ export class Node {
return visit(); return visit();
case "CallExpr": case "CallExpr":
return visit(k.expr, ...k.args); return visit(k.expr, ...k.args);
case "BinaryExpr":
return visit(k.left, k.right);
case "IdentTy": case "IdentTy":
return visit(); return visit();
} }
@ -106,8 +108,29 @@ export type NodeKind =
| { tag: "IdentExpr"; ident: string } | { tag: "IdentExpr"; ident: string }
| { tag: "IntExpr"; value: number } | { tag: "IntExpr"; value: number }
| { tag: "CallExpr"; expr: Node; args: Node[] } | { tag: "CallExpr"; expr: Node; args: Node[] }
| { tag: "BinaryExpr"; op: BinaryOp; left: Node; right: Node; tok: string }
| { tag: "IdentTy"; ident: 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 { export interface Visitor {
visit(node: Node): void | "break"; visit(node: Node): void | "break";
} }

View File

@ -1,23 +1,6 @@
import * as ast from "./ast.ts"; import * as ast from "./ast.ts";
import { Ty } from "./ty.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 { export class Checker {
private nodeTys = new Map<number, Ty>(); private nodeTys = new Map<number, Ty>();
@ -76,7 +59,7 @@ export class Checker {
return this.check(sym.stmt); return this.check(sym.stmt);
} }
if (sym.tag === "Builtin") { 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") { if (sym.tag === "FnParam") {
return this.check(sym.param); return this.check(sym.param);
@ -95,6 +78,25 @@ export class Checker {
return this.checkCall(node); 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")) { if (node.is("IdentTy")) {
switch (node.kind.ident) { switch (node.kind.ident) {
case "void": 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 = export type Sym =
| { tag: "Error" } | { tag: "Error" }
| { tag: "Builtin"; id: string } | { tag: "Builtin"; id: string }
@ -264,7 +278,7 @@ class ResolverSyms {
static root(): ResolverSyms { static root(): ResolverSyms {
return new ResolverSyms( return new ResolverSyms(
new Map( new Map(
rootSyms.map<[string, Sym]>((sym) => [ builtins.map<[string, Sym]>((sym) => [
sym.id, sym.id,
{ tag: "Builtin", id: sym.id }, { tag: "Builtin", id: sym.id },
]), ]),
@ -365,6 +379,28 @@ export function resolve(
return new ResolveMap(resols); 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( export function parse(
filename: string, filename: string,
text: string, text: string,
@ -480,6 +516,57 @@ export class Parser {
} }
parseExpr(): ast.Node { 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(); return this.parsePostfix();
} }
@ -588,13 +675,15 @@ export class Parser {
export type Tok = { type: string; value: string; line: number }; export type Tok = { type: string; value: string; line: number };
const keywordRegex = const keywordPattern =
/^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)$/; /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)$/;
const operatorPattern =
/((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g;
export function tokenize(text: string): Tok[] { export function tokenize(text: string): Tok[] {
return text return text
.replace(/\/\/[^\n]*/g, "") .replace(/\/\/[^\n]*/g, "")
.replace(/((?:\-\>)|[\n\(\)\{\},.;:])/g, " $1 ") .replace(operatorPattern, " $1 ")
.split(/[ \t\r]/) .split(/[ \t\r]/)
.filter((value) => value !== "") .filter((value) => value !== "")
.reduce<[[string, number][], number]>( .reduce<[[string, number][], number]>(
@ -612,7 +701,7 @@ export function tokenize(text: string): Tok[] {
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tok.value) /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tok.value)
? { ? {
...tok, ...tok,
type: keywordRegex.test(tok.value) ? tok.value : "ident", type: keywordPattern.test(tok.value) ? tok.value : "ident",
} }
: tok : tok
) )

View File

@ -34,7 +34,9 @@ if (!mainFn) {
const m = new middle.MiddleLowerer(resols, checker); const m = new middle.MiddleLowerer(resols, checker);
const mainMiddleFn = m.lowerFn(mainFn); const mainMiddleFn = m.lowerFn(mainFn);
console.log(mainMiddleFn.pretty()); if (!Deno.args.includes("--test")) {
console.log(mainMiddleFn.pretty());
}
const interp = new MirInterpreter(); const interp = new MirInterpreter();
interp.eval(mainMiddleFn); interp.eval(mainMiddleFn);

View File

@ -63,6 +63,12 @@ class FnLowerer {
this.pushInst(Ty.Void, "Return", { source }); this.pushInst(Ty.Void, "Return", { source });
return; 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")) { if (stmt.is("ExprStmt")) {
this.lowerExpr(stmt.kind.expr); this.lowerExpr(stmt.kind.expr);
return; return;
@ -70,6 +76,21 @@ class FnLowerer {
throw new Error(`'${stmt.kind.tag}' not handled`); 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 { private lowerExpr(expr: ast.Node): Inst {
if (expr.is("IdentExpr")) { if (expr.is("IdentExpr")) {
const sym = this.resols.get(expr); const sym = this.resols.get(expr);
@ -118,6 +139,19 @@ class FnLowerer {
const callee = this.lowerExpr(expr.kind.expr); const callee = this.lowerExpr(expr.kind.expr);
return this.pushInst(ty, "Call", { callee, args }); 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`); 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 { export class Fn {
constructor( constructor(
public stmt: ast.FnStmt, public stmt: ast.FnStmt,
@ -237,7 +282,22 @@ export class Inst {
return ` ${r(k.target)}, ${r(k.source)}`; return ` ${r(k.target)}, ${r(k.source)}`;
case "Return": case "Return":
return ` ${r(k.source)}`; 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 "Add":
case "Sub":
case "Mul":
case "Div":
case "Rem":
return ` ${r(k.left)} ${r(k.right)}`; return ` ${r(k.left)} ${r(k.right)}`;
case "DebugPrint": case "DebugPrint":
return ` ${k.args.map(r).join(", ")}`; return ` ${k.args.map(r).join(", ")}`;
@ -259,5 +319,23 @@ export type InstKind =
| { tag: "LocalLoad"; source: Inst } | { tag: "LocalLoad"; source: Inst }
| { tag: "LocalStore"; target: Inst; source: Inst } | { tag: "LocalStore"; target: Inst; source: Inst }
| { tag: "Return"; source: Inst } | { tag: "Return"; source: Inst }
| { tag: "Add"; left: Inst; right: Inst } | { tag: BinaryOp; left: Inst; right: Inst }
| { tag: "DebugPrint"; args: Inst[] }; | { tag: "DebugPrint"; args: Inst[] };
export type BinaryOp =
| "Eq"
| "Ne"
| "Lt"
| "Gt"
| "Lte"
| "Gte"
| "BitOr"
| "BitXor"
| "BitAnd"
| "Shl"
| "Shr"
| "Add"
| "Sub"
| "Mul"
| "Div"
| "Rem";

View File

@ -57,26 +57,63 @@ export class MirInterpreter {
continue; continue;
case "Return": case "Return":
return regs.get(k.source)!; 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 left = regs.get(k.left)!;
const right = regs.get(k.right)!; const right = regs.get(k.right)!;
if (left.kind.tag === "Int" && right.kind.tag == "Int") { const lk = left.kind;
regs.set( const rk = right.kind;
inst,
new Val({ if (lk.tag === "Int" && rk.tag === "Int") {
tag: "Int", const value = (() => {
value: left.kind.value + right.kind.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; continue;
} }
throw new Error(); throw new Error(`'${k.tag}' not handled`);
} }
case "DebugPrint": case "DebugPrint":
console.log( console.log(
`debug: ${ k.args
k.args.map((a) => regs.get(a)!.pretty()).join(", ") .map((a) => regs.get(a)!.pretty())
}`, .join(", "),
); );
continue; continue;
} }

9
tests/assign.ethlang Normal file
View File

@ -0,0 +1,9 @@
// expect: 456
fn main()
{
let v: int = 123;
v = 456;
print_int(v);
}

10
tests/fn_int.ethlang Normal file
View File

@ -0,0 +1,10 @@
fn my_int_fn() -> int {
return 123;
}
fn main()
{
my_int_fn();
}

12
tests/fn_void.ethlang Normal file
View File

@ -0,0 +1,12 @@
fn my_implicit_void() {}
fn my_explicit_void() -> void {}
fn main()
{
my_implicit_void();
my_explicit_void();
}

8
tests/let.ethlang Normal file
View File

@ -0,0 +1,8 @@
fn main()
{
let a = 123;
let b: int = 321;
let c = b;
}

12
tests/operators.ethlang Normal file
View File

@ -0,0 +1,12 @@
// expect: 8
// expect: 2
fn main()
{
let a = 5;
let b = 3;
print_int(a + b);
print_int(a - b);
}

61
tests/test.sh Executable file
View File

@ -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