added unary ops
All checks were successful
Check / Explore-Gitea-Actions (push) Successful in 8s

This commit is contained in:
sfja 2026-03-11 19:21:22 +01:00
parent 2dff17144c
commit 856618d113
7 changed files with 100 additions and 3 deletions

View File

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

View File

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

View File

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

View File

@ -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[] };

View File

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

View File

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

View File

@ -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);
}
}