From 856618d113e5ceb868d6f452119a50217a2faafa Mon Sep 17 00:00:00 2001 From: sfja Date: Wed, 11 Mar 2026 19:21:22 +0100 Subject: [PATCH] added unary ops --- src/ast.ts | 7 +++++++ src/front/check.ts | 15 +++++++++++++++ src/front/parse.ts | 17 ++++++++++++++--- src/middle.ts | 28 ++++++++++++++++++++++++++++ src/mir_interpreter.ts | 22 ++++++++++++++++++++++ src/ty.ts | 5 +++++ tests/operators.ethlang | 9 +++++++++ 7 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index e023866..f344592 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -82,6 +82,8 @@ export class Node { return visit(); case "CallExpr": return visit(k.expr, ...k.args); + case "UnaryExpr": + return visit(k.expr); case "BinaryExpr": return visit(k.left, k.right); case "IdentTy": @@ -111,9 +113,14 @@ export type NodeKind = | { tag: "IdentExpr"; ident: string } | { tag: "IntExpr"; value: number } | { tag: "CallExpr"; expr: Node; args: Node[] } + | { tag: "UnaryExpr"; op: UnaryOp; expr: Node; tok: string } | { tag: "BinaryExpr"; op: BinaryOp; left: Node; right: Node; tok: string } | { tag: "IdentTy"; ident: string }; +export type UnaryOp = + | "Not" + | "Negate"; + export type BinaryOp = | "Or" | "And" diff --git a/src/front/check.ts b/src/front/check.ts index b677a4c..9963fc0 100644 --- a/src/front/check.ts +++ b/src/front/check.ts @@ -84,6 +84,21 @@ export class Checker { return this.checkCall(node); } + if (node.is("UnaryExpr")) { + const expr = this.check(node.kind.expr); + if (node.kind.op === "Negate" && expr.compatibleWith(Ty.Int)) { + return Ty.Int; + } + if (node.kind.op === "Not" && expr.compatibleWith(Ty.Bool)) { + return Ty.Bool; + } + this.error( + node.line, + `operator '${node.kind.tok}' cannot be applied to type '${expr.pretty()}'`, + ); + this.fail(); + } + if (node.is("BinaryExpr")) { const left = this.check(node.kind.left); const right = this.check(node.kind.right); diff --git a/src/front/parse.ts b/src/front/parse.ts index 95508e3..d4af4bb 100644 --- a/src/front/parse.ts +++ b/src/front/parse.ts @@ -136,7 +136,7 @@ export class Parser { parseBinary(prec = 7): ast.Node { const loc = this.loc(); if (prec == 0) { - return this.parsePrefixE(); + return this.parsePrefix(); } const ops: [Tok["type"], ast.BinaryOp, number][] = [ ["or", "Or", 9], @@ -180,7 +180,18 @@ export class Parser { return left; } - parsePrefixE() { + parsePrefix(): ast.Node { + const loc = this.loc(); + const ops: [Tok["type"], ast.UnaryOp][] = [ + ["not", "Not"], + ["-", "Negate"], + ]; + for (const [tok, op] of ops) { + if (this.eat(tok)) { + const expr = this.parsePrefix(); + return ast.Node.create(loc, "UnaryExpr", { op, expr, tok }); + } + } return this.parsePostfix(); } @@ -290,7 +301,7 @@ export class Parser { export type Tok = { type: string; value: string; line: number }; const keywordPattern = - /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)$/; + /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)|(?:not)$/; const operatorPattern = /((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g; diff --git a/src/middle.ts b/src/middle.ts index e3d791e..e9251e1 100644 --- a/src/middle.ts +++ b/src/middle.ts @@ -180,6 +180,29 @@ class FnLowerer { const callee = this.lowerExpr(expr.kind.expr); return this.pushInst(ty, "Call", { callee, args }); } + if (expr.is("UnaryExpr")) { + const resultTy = this.checker.check(expr); + const operandTy = this.checker.check(expr.kind.expr); + if ( + expr.kind.op === "Negate" && + operandTy.compatibleWith(Ty.Int) && + resultTy.compatibleWith(Ty.Int) + ) { + const operand = this.lowerExpr(expr.kind.expr); + return this.pushInst(Ty.Int, "Negate", { source: operand }); + } + if ( + expr.kind.op === "Not" && + operandTy.compatibleWith(Ty.Bool) && + resultTy.compatibleWith(Ty.Bool) + ) { + const operand = this.lowerExpr(expr.kind.expr); + return this.pushInst(Ty.Bool, "Not", { source: operand }); + } + throw new Error( + `'${expr.kind.op}' with '${resultTy.pretty()}' not handled`, + ); + } if (expr.is("BinaryExpr")) { const resultTy = this.checker.check(expr); const leftTy = this.checker.check(expr.kind.left); @@ -349,6 +372,9 @@ export class Inst { }`; case "Return": return ` ${r(k.source)}`; + case "Not": + case "Negate": + return ` ${r(k.source)}`; case "Eq": case "Ne": case "Lt": @@ -389,6 +415,8 @@ export type InstKind = | { tag: "Jump"; target: BasicBlock } | { tag: "Branch"; cond: Inst; truthy: BasicBlock; falsy: BasicBlock } | { tag: "Return"; source: Inst } + | { tag: "Not"; source: Inst } + | { tag: "Negate"; source: Inst } | { tag: BinaryOp; left: Inst; right: Inst } | { tag: "DebugPrint"; args: Inst[] }; diff --git a/src/mir_interpreter.ts b/src/mir_interpreter.ts index 41f0da8..d189c37 100644 --- a/src/mir_interpreter.ts +++ b/src/mir_interpreter.ts @@ -82,6 +82,28 @@ export class FnInterpreter { } case "Return": return this.regs.get(k.source)!; + case "Not": { + const source = this.regs.get(k.source)!; + if (source.kind.tag !== "Bool") { + throw new Error(); + } + this.regs.set( + inst, + new Val({ tag: "Bool", value: !source.kind.value }), + ); + break; + } + case "Negate": { + const source = this.regs.get(k.source)!; + if (source.kind.tag !== "Int") { + throw new Error(); + } + this.regs.set( + inst, + new Val({ tag: "Int", value: -source.kind.value }), + ); + break; + } case "Eq": case "Ne": case "Lt": diff --git a/src/ty.ts b/src/ty.ts index 82a99d3..de3de7d 100644 --- a/src/ty.ts +++ b/src/ty.ts @@ -42,6 +42,11 @@ export class Ty { } compatibleWith(other: Ty): boolean { + // types are interned--we can just do this + if (this === other) { + return true; + } + if (this.is("Error")) { return false; } diff --git a/tests/operators.ethlang b/tests/operators.ethlang index e35964d..7aaf880 100644 --- a/tests/operators.ethlang +++ b/tests/operators.ethlang @@ -3,6 +3,8 @@ // expect: 15 // expect: 7 // expect: 2 +// expect: -5 +// expect: 123 fn main() { @@ -14,5 +16,12 @@ fn main() print_int(a * b); print_int(a * b / 2); print_int(a % b); + print_int(-a); + + let c = false; + + if not c { + print_int(123); + } }