tests + binary ops
This commit is contained in:
parent
3b3a189020
commit
bdee8b5bed
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
23
src/ast.ts
23
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";
|
||||
}
|
||||
|
||||
135
src/front.ts
135
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<number, Ty>();
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -34,7 +34,9 @@ if (!mainFn) {
|
||||
const m = new middle.MiddleLowerer(resols, checker);
|
||||
const mainMiddleFn = m.lowerFn(mainFn);
|
||||
|
||||
if (!Deno.args.includes("--test")) {
|
||||
console.log(mainMiddleFn.pretty());
|
||||
}
|
||||
|
||||
const interp = new MirInterpreter();
|
||||
interp.eval(mainMiddleFn);
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
9
tests/assign.ethlang
Normal file
9
tests/assign.ethlang
Normal 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
10
tests/fn_int.ethlang
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
fn my_int_fn() -> int {
|
||||
return 123;
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
my_int_fn();
|
||||
}
|
||||
|
||||
12
tests/fn_void.ethlang
Normal file
12
tests/fn_void.ethlang
Normal 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
8
tests/let.ethlang
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
fn main()
|
||||
{
|
||||
let a = 123;
|
||||
let b: int = 321;
|
||||
let c = b;
|
||||
}
|
||||
|
||||
12
tests/operators.ethlang
Normal file
12
tests/operators.ethlang
Normal 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
61
tests/test.sh
Executable 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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user