diff --git a/src/ast.ts b/src/ast.ts index 5ff0085..5de513c 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -84,6 +84,10 @@ export class Node { return visit(k.param, k.expr); case "IfStmt": return visit(k.cond, k.truthy, k.falsy); + case "WhileStmt": + return visit(k.cond, k.body); + case "BreakStmt": + return visit(); case "Param": return visit(k.ty); case "IdentExpr": @@ -132,6 +136,8 @@ export type NodeKind = | { tag: "ReturnStmt"; expr: Node | null } | { tag: "LetStmt"; param: Node; expr: Node } | { tag: "IfStmt"; cond: Node; truthy: Node; falsy: Node | null } + | { tag: "WhileStmt"; cond: Node; body: Node } + | { tag: "BreakStmt" } | { tag: "Param"; ident: string; ty: Node | null } | { tag: "IdentExpr"; ident: string } | { tag: "IntExpr"; value: number } diff --git a/src/front/parse.ts b/src/front/parse.ts index 413475f..22a28cd 100644 --- a/src/front/parse.ts +++ b/src/front/parse.ts @@ -51,6 +51,10 @@ export class Parser { return this.parseLetStmt(); } else if (this.test("if")) { return this.parseIfStmt(); + } else if (this.test("while")) { + return this.parseWhileStmt(); + } else if (this.test("break")) { + return this.parseBreakStmt(); } else { const place = this.parseExpr(); if (this.eat("=")) { @@ -120,6 +124,21 @@ export class Parser { return ast.Node.create(loc, "IfStmt", { cond, truthy, falsy }); } + parseWhileStmt(): ast.Node { + const loc = this.loc(); + this.step(); + const cond = this.parseExpr(); + const body = this.parseBlock(); + return ast.Node.create(loc, "WhileStmt", { cond, body }); + } + + parseBreakStmt(): ast.Node { + const loc = this.loc(); + this.step(); + this.mustEat(";"); + return ast.Node.create(loc, "BreakStmt", {}); + } + parseParam(): ast.Node { const loc = this.loc(); const ident = this.mustEat("ident").value; @@ -322,22 +341,24 @@ export class Parser { private mustEat(type: string, loc = this.loc()): Tok { const tok = this.current; if (tok.type !== type) { - this.error( + this.reporter.error( + loc, `expected '${type}', got '${ this.done ? "eof" : this.current.type }'`, - loc, ); + if (type === ";" && this.idx > 0) { + this.reporter.info( + this.toks[this.idx - 1].loc, + `try adding '${type}' here`, + ); + } + this.reporter.abort(); } this.step(); return tok; } - private error(message: string, loc: Loc): never { - this.reporter.error(loc, message); - this.reporter.abort(); - } - private eat(type: string): boolean { if (this.test(type)) { this.step(); diff --git a/src/front/resolve.ts b/src/front/resolve.ts index 3cec817..b746e67 100644 --- a/src/front/resolve.ts +++ b/src/front/resolve.ts @@ -30,7 +30,8 @@ export type Sym = tag: "Let"; stmt: ast.NodeWithKind<"LetStmt">; param: ast.NodeWithKind<"Param">; - }; + } + | { tag: "Loop"; stmt: ast.Node }; export function resolve( file: ast.Node, @@ -39,6 +40,8 @@ export function resolve( let syms = ResolverSyms.root(); const resols = new Map(); + const loopStack: ast.Node[] = []; + file.visit({ visit(node) { const k = node.kind; @@ -85,6 +88,22 @@ export function resolve( } resols.set(node.id, sym); } + + if (k.tag === "WhileStmt") { + loopStack.push(node); + node.visitBelow(this); + loopStack.pop(); + return "break"; + } + + if (k.tag === "BreakStmt") { + const loopNode = loopStack.at(-1); + if (!loopNode) { + reporter.error(node.loc, `break outside loop`); + reporter.abort(); + } + resols.set(node.id, { tag: "Loop", stmt: loopNode }); + } }, }); diff --git a/src/middle.ts b/src/middle.ts index cf3d4fd..b08df4e 100644 --- a/src/middle.ts +++ b/src/middle.ts @@ -25,6 +25,7 @@ class FnLowerer { private allocs: Inst[] = []; private bbs: BasicBlock[] = [new BasicBlock()]; private localMap = new Map(); + private loopEndMap = new Map(); constructor( private lowerer: MiddleLowerer, @@ -68,6 +69,7 @@ class FnLowerer { ? this.lowerExpr(stmt.kind.expr) : this.makeVoid(); this.pushInst(Ty.Void, "Return", { source }); + this.bbs.push(new BasicBlock()); return; } if (stmt.is("IfStmt")) { @@ -108,6 +110,54 @@ class FnLowerer { ); return; } + if (stmt.is("WhileStmt")) { + const before = this.bbs.at(-1)!; + + this.bbs.push(new BasicBlock()); + const body = this.bbs.at(-1)!; + + const after = new BasicBlock(); + this.loopEndMap.set(stmt.id, after); + this.lowerBlock(stmt.kind.body.as("Block")); + + const bodyEnd = this.bbs.at(-1)!; + + this.bbs.push(new BasicBlock()); + const condBlock = this.bbs.at(-1)!; + const cond = this.lowerExpr(stmt.kind.cond); + const condBlockEnd = this.bbs.at(-1)!; + + this.bbs.push(after); + + before.insts.push( + new Inst(Ty.Void, { tag: "Jump", target: condBlock }), + ); + condBlockEnd.insts.push( + new Inst(Ty.Void, { + tag: "Branch", + cond: cond, + truthy: body, + falsy: after, + }), + ); + bodyEnd.insts.push( + new Inst(Ty.Void, { tag: "Jump", target: condBlock }), + ); + return; + } + if (stmt.is("BreakStmt")) { + const sym = this.syms.get(stmt); + if (sym.tag !== "Loop") { + throw new Error(); + } + const loopEnd = this.loopEndMap.get(sym.stmt.id); + if (!loopEnd) { + throw new Error(); + } + this.pushInst(Ty.Void, "Jump", { target: loopEnd }); + this.bbs.push(new BasicBlock()); + return; + } if (stmt.is("AssignStmt")) { const source = this.lowerExpr(stmt.kind.expr); const target = this.lowerPlace(stmt.kind.place); diff --git a/tests/if.ethlang b/tests/if.ethlang index afc4d30..68c4bf5 100644 --- a/tests/if.ethlang +++ b/tests/if.ethlang @@ -1,4 +1,5 @@ // expect: 2 +// expect: 5 fn main() { @@ -8,5 +9,17 @@ fn main() a = 2; } + if false { + a = 3; + } + + print_int(a); + + if false { + a = 4; + } else { + a = 5; + } + print_int(a); } diff --git a/tests/loop.ethlang b/tests/loop.ethlang new file mode 100644 index 0000000..2f696ab --- /dev/null +++ b/tests/loop.ethlang @@ -0,0 +1,22 @@ +// expect: 3 +// expect: 8 + +fn main() +{ + let a = 0; + + while a < 3 { + a = a + 1; + } + + print_int(a); + + while a < 10 { + if a >= 8 { + break; + } + a = a + 1; + } + + print_int(a); +}