add pointers
All checks were successful
Check / Explore-Gitea-Actions (push) Successful in 7s

This commit is contained in:
sfja 2026-03-11 23:33:09 +01:00
parent 27b4e89694
commit cf36151115
8 changed files with 173 additions and 41 deletions

View File

@ -88,6 +88,9 @@ export class Node {
return visit(k.left, k.right); return visit(k.left, k.right);
case "IdentTy": case "IdentTy":
return visit(); return visit();
case "PtrTy":
case "PtrMutTy":
return visit(k.ty);
} }
k satisfies never; k satisfies never;
} }
@ -115,11 +118,15 @@ export type NodeKind =
| { tag: "CallExpr"; expr: Node; args: Node[] } | { tag: "CallExpr"; expr: Node; args: Node[] }
| { tag: "UnaryExpr"; op: UnaryOp; expr: Node; tok: string } | { tag: "UnaryExpr"; op: UnaryOp; expr: Node; tok: string }
| { tag: "BinaryExpr"; op: BinaryOp; left: Node; right: Node; tok: string } | { tag: "BinaryExpr"; op: BinaryOp; left: Node; right: Node; tok: string }
| { tag: "IdentTy"; ident: string }; | { tag: "IdentTy"; ident: string }
| { tag: "PtrTy" | "PtrMutTy"; ty: Node };
export type UnaryOp = export type UnaryOp =
| "Not" | "Not"
| "Negate"; | "Negate"
| "Ref"
| "RefMut"
| "Deref";
export type BinaryOp = export type BinaryOp =
| "Or" | "Or"

View File

@ -85,16 +85,27 @@ export class Checker {
} }
if (node.is("UnaryExpr")) { if (node.is("UnaryExpr")) {
const expr = this.check(node.kind.expr); const exprTy = this.check(node.kind.expr);
if (node.kind.op === "Negate" && expr.compatibleWith(Ty.Int)) { if (node.kind.op === "Negate" && exprTy.compatibleWith(Ty.Int)) {
return Ty.Int; return Ty.Int;
} }
if (node.kind.op === "Not" && expr.compatibleWith(Ty.Bool)) { if (node.kind.op === "Not" && exprTy.compatibleWith(Ty.Bool)) {
return Ty.Bool; return Ty.Bool;
} }
if (node.kind.op === "Ref") {
return Ty.create("Ptr", { ty: exprTy });
}
if (node.kind.op === "RefMut") {
return Ty.create("PtrMut", { ty: exprTy });
}
if (node.kind.op === "Deref") {
if (exprTy.is("Ptr") || exprTy.is("PtrMut")) {
return exprTy.kind.ty;
}
}
this.error( this.error(
node.line, node.line,
`operator '${node.kind.tok}' cannot be applied to type '${expr.pretty()}'`, `operator '${node.kind.tok}' cannot be applied to type '${exprTy.pretty()}'`,
); );
this.fail(); this.fail();
} }
@ -131,6 +142,15 @@ export class Checker {
} }
} }
if (node.is("PtrTy")) {
const ty = this.check(node.kind.ty);
return Ty.create("Ptr", { ty });
}
if (node.is("PtrMutTy")) {
const ty = this.check(node.kind.ty);
return Ty.create("PtrMut", { ty });
}
throw new Error(`'${k.tag}' not unhandled`); throw new Error(`'${k.tag}' not unhandled`);
} }

View File

@ -185,6 +185,7 @@ export class Parser {
const ops: [Tok["type"], ast.UnaryOp][] = [ const ops: [Tok["type"], ast.UnaryOp][] = [
["not", "Not"], ["not", "Not"],
["-", "Negate"], ["-", "Negate"],
["*", "Deref"],
]; ];
for (const [tok, op] of ops) { for (const [tok, op] of ops) {
if (this.eat(tok)) { if (this.eat(tok)) {
@ -192,6 +193,12 @@ export class Parser {
return ast.Node.create(loc, "UnaryExpr", { op, expr, tok }); return ast.Node.create(loc, "UnaryExpr", { op, expr, tok });
} }
} }
if (this.eat("&")) {
const op: ast.UnaryOp = this.eat("mut") ? "RefMut" : "Ref";
const expr = this.parsePrefix();
const tok = op === "Ref" ? "&" : "&mut";
return ast.Node.create(loc, "UnaryExpr", { op, expr, tok });
}
return this.parsePostfix(); return this.parsePostfix();
} }
@ -245,6 +252,10 @@ export class Parser {
const ident = this.current.value; const ident = this.current.value;
this.step(); this.step();
return ast.Node.create(loc, "IdentTy", { ident }); return ast.Node.create(loc, "IdentTy", { ident });
} else if (this.eat("*")) {
const mutable = this.eat("mut");
const ty = this.parseTy();
return ast.Node.create(loc, mutable ? "PtrMutTy" : "PtrTy", { ty });
} else { } else {
this.mustEat("<type>"); this.mustEat("<type>");
throw new Error(); throw new Error();
@ -305,7 +316,7 @@ export class Parser {
export type Tok = { type: string; value: string; line: number }; export type Tok = { type: string; value: string; line: number };
const keywordPattern = const keywordPattern =
/^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)|(?:not)$/; /^(?:fn)|(?:return)|(?:let)|(?:if)|(?:else)|(?:while)|(?:break)|(?:or)|(?:and)|(?:not)|(?:mut)$/;
const operatorPattern = const operatorPattern =
/((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g; /((?:\->)|(?:==)|(?:!=)|(?:<=)|(?:>=)|(?:<<)|(?:>>)|[\n\(\)\{\}\,\.\;\:\!\=\<\>\&\^\|\+\-\*\/\%])/g;

View File

@ -21,6 +21,7 @@ export class MiddleLowerer {
} }
class FnLowerer { class FnLowerer {
private allocs: Inst[] = [];
private bbs: BasicBlock[] = [new BasicBlock()]; private bbs: BasicBlock[] = [new BasicBlock()];
private localMap = new Map<number, Inst>(); private localMap = new Map<number, Inst>();
@ -35,6 +36,7 @@ class FnLowerer {
const ty = this.checker.check(this.stmt); const ty = this.checker.check(this.stmt);
this.lowerBlock(this.stmt.kind.body.as("Block")); this.lowerBlock(this.stmt.kind.body.as("Block"));
this.pushInst(Ty.Void, "Return", { source: this.makeVoid() }); this.pushInst(Ty.Void, "Return", { source: this.makeVoid() });
this.bbs[0].insts.unshift(...this.allocs);
return new Fn(this.stmt, ty, this.bbs); return new Fn(this.stmt, ty, this.bbs);
} }
@ -48,8 +50,9 @@ class FnLowerer {
if (stmt.is("LetStmt")) { if (stmt.is("LetStmt")) {
const ty = this.checker.check(stmt.kind.param); const ty = this.checker.check(stmt.kind.param);
const expr = this.lowerExpr(stmt.kind.expr); const expr = this.lowerExpr(stmt.kind.expr);
const local = this.pushInst(ty, "AllocLocal", {}); const local = new Inst(ty, { tag: "Alloca" });
this.pushInst(Ty.Void, "LocalStore", { this.allocs.push(local);
this.pushInst(Ty.Void, "Store", {
target: local, target: local,
source: expr, source: expr,
}); });
@ -103,8 +106,8 @@ class FnLowerer {
} }
if (stmt.is("AssignStmt")) { if (stmt.is("AssignStmt")) {
const source = this.lowerExpr(stmt.kind.expr); const source = this.lowerExpr(stmt.kind.expr);
const target = this.lowerAssignPlace(stmt.kind.place); const target = this.lowerPlace(stmt.kind.place);
this.pushInst(Ty.Void, "LocalStore", { target, source }); this.pushInst(Ty.Void, "Store", { target, source });
return; return;
} }
if (stmt.is("ExprStmt")) { if (stmt.is("ExprStmt")) {
@ -114,7 +117,7 @@ class FnLowerer {
throw new Error(`'${stmt.kind.tag}' not handled`); throw new Error(`'${stmt.kind.tag}' not handled`);
} }
private lowerAssignPlace(place: ast.Node): Inst { private lowerPlace(place: ast.Node): Inst {
if (place.is("IdentExpr")) { if (place.is("IdentExpr")) {
const sym = this.resols.get(place); const sym = this.resols.get(place);
if (sym.tag === "Let") { if (sym.tag === "Let") {
@ -126,6 +129,11 @@ class FnLowerer {
} }
throw new Error(`'${sym.tag}' not handled`); throw new Error(`'${sym.tag}' not handled`);
} }
if (place.is("UnaryExpr") && place.kind.op === "Deref") {
return this.lowerExpr(place.kind.expr);
}
throw new Error(`'${place.kind.tag}' not handled`); throw new Error(`'${place.kind.tag}' not handled`);
} }
@ -148,7 +156,7 @@ class FnLowerer {
if (!local) { if (!local) {
throw new Error(); throw new Error();
} }
return this.pushInst(local.ty, "LocalLoad", { source: local }); return this.pushInst(local.ty, "Load", { source: local });
} }
if (sym.tag === "Bool") { if (sym.tag === "Bool") {
return this.pushInst(Ty.Bool, "Bool", { value: sym.value }); return this.pushInst(Ty.Bool, "Bool", { value: sym.value });
@ -199,6 +207,29 @@ class FnLowerer {
const operand = this.lowerExpr(expr.kind.expr); const operand = this.lowerExpr(expr.kind.expr);
return this.pushInst(Ty.Bool, "Not", { source: operand }); return this.pushInst(Ty.Bool, "Not", { source: operand });
} }
if (expr.kind.op === "Ref" || expr.kind.op === "RefMut") {
const place = expr.kind.expr;
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(
`${expr.kind.op} with sym ${sym.tag} not handled`,
);
}
throw new Error(
`${expr.kind.op} with place ${place.kind.tag} not handled`,
);
}
if (expr.kind.op === "Deref") {
const source = this.lowerExpr(expr.kind.expr);
return this.pushInst(resultTy, "Load", { source });
}
throw new Error( throw new Error(
`'${expr.kind.op}' with '${resultTy.pretty()}' not handled`, `'${expr.kind.op}' with '${resultTy.pretty()}' not handled`,
); );
@ -266,6 +297,12 @@ const binaryOpPatterns: BinaryOpPattern[] = [
{ op: "Gte", tag: "Gte", result: Ty.Bool, left: Ty.Int }, { op: "Gte", tag: "Gte", result: Ty.Bool, left: Ty.Int },
]; ];
export interface Visitor {
visitFn?(fn: Fn): void;
visitBasicBlock?(bb: BasicBlock): void;
visitInst?(inst: Inst): void;
}
export class Fn { export class Fn {
constructor( constructor(
public stmt: ast.FnStmt, public stmt: ast.FnStmt,
@ -273,6 +310,13 @@ export class Fn {
public bbs: BasicBlock[], public bbs: BasicBlock[],
) {} ) {}
visit(v: Visitor) {
v.visitFn?.(this);
for (const bb of this.bbs) {
bb.visit(v);
}
}
pretty(): string { pretty(): string {
const fnTy = this.ty.is("FnStmt") && this.ty.kind.ty.is("Fn") const fnTy = this.ty.is("FnStmt") && this.ty.kind.ty.is("Fn")
? this.ty.kind.ty ? this.ty.kind.ty
@ -320,6 +364,13 @@ class PrettyCx {
export class BasicBlock { export class BasicBlock {
public insts: Inst[] = []; public insts: Inst[] = [];
visit(v: Visitor) {
v.visitBasicBlock?.(this);
for (const inst of this.insts) {
inst.visit(v);
}
}
pretty(cx: PrettyCx): string { pretty(cx: PrettyCx): string {
return `bb${cx.bbId(this)}:\n${ return `bb${cx.bbId(this)}:\n${
this.insts this.insts
@ -336,8 +387,12 @@ export class Inst {
public kind: InstKind, public kind: InstKind,
) {} ) {}
visit(v: Visitor) {
v.visitInst?.(this);
}
pretty(cx: PrettyCx): string { pretty(cx: PrettyCx): string {
const r = (v: Inst) => `_${cx.regId(v)}`; const r = (v: Inst) => `%${cx.regId(v)}`;
return `${`${r(this)}:`.padEnd(4, " ")} ${ return `${`${r(this)}:`.padEnd(4, " ")} ${
this.ty.pretty().padEnd(4, " ") this.ty.pretty().padEnd(4, " ")
@ -358,18 +413,18 @@ export class Inst {
return ` ${k.idx}`; return ` ${k.idx}`;
case "Call": case "Call":
return ` ${r(k.callee)} (${k.args.map(r).join(", ")})`; return ` ${r(k.callee)} (${k.args.map(r).join(", ")})`;
case "AllocLocal": case "Alloca":
return ""; return "";
case "LocalLoad": case "Load":
return ` ${r(k.source)}`; return ` ${r(k.source)}`;
case "LocalStore": case "Store":
return ` ${r(k.target)} = ${r(k.source)}`; return ` ${r(k.target)} = ${r(k.source)}`;
case "Jump": case "Jump":
return ` bb${cx.bbId(k.target)}`; return ` bb${cx.bbId(k.target)}`;
case "Branch": case "Branch":
return ` ${r(k.cond)} ? bb${cx.bbId(k.truthy)} : bb${ return ` if ${r(k.cond)} then bb${
cx.bbId(k.falsy) cx.bbId(k.truthy)
}`; } else bb${cx.bbId(k.falsy)}`;
case "Return": case "Return":
return ` ${r(k.source)}`; return ` ${r(k.source)}`;
case "Not": case "Not":
@ -409,9 +464,9 @@ export type InstKind =
| { tag: "Fn"; fn: Fn } | { tag: "Fn"; fn: Fn }
| { tag: "Param"; idx: number } | { tag: "Param"; idx: number }
| { tag: "Call"; callee: Inst; args: Inst[] } | { tag: "Call"; callee: Inst; args: Inst[] }
| { tag: "AllocLocal" } | { tag: "Alloca" }
| { tag: "LocalLoad"; source: Inst } | { tag: "Load"; source: Inst }
| { tag: "LocalStore"; target: Inst; source: Inst } | { tag: "Store"; target: Inst; source: Inst }
| { tag: "Jump"; target: BasicBlock } | { tag: "Jump"; target: BasicBlock }
| { tag: "Branch"; cond: Inst; truthy: BasicBlock; falsy: BasicBlock } | { tag: "Branch"; cond: Inst; truthy: BasicBlock; falsy: BasicBlock }
| { tag: "Return"; source: Inst } | { tag: "Return"; source: Inst }

View File

@ -3,7 +3,6 @@ import * as mir from "./middle.ts";
export class FnInterpreter { export class FnInterpreter {
private regs = new Map<mir.Inst, Val>(); private regs = new Map<mir.Inst, Val>();
private locals: (Val | null)[] = []; private locals: (Val | null)[] = [];
private localMap = new Map<mir.Inst, number>();
private bb: mir.BasicBlock; private bb: mir.BasicBlock;
private instIdx = 0; private instIdx = 0;
@ -42,30 +41,36 @@ export class FnInterpreter {
this.regs.set(inst, val); this.regs.set(inst, val);
break; break;
} }
case "AllocLocal": case "Alloca": {
this.localMap.set(inst, this.locals.length); const localIdx = this.locals.length;
this.locals.push(null); this.locals.push(null);
break;
case "LocalLoad":
if (!this.localMap.has(k.source)) {
throw new Error();
}
if (this.locals[this.localMap.get(k.source)!] === null) {
throw new Error();
}
this.regs.set( this.regs.set(
inst, inst,
this.locals[this.localMap.get(k.source)!]!, new Val({ tag: "LocalPtr", localIdx, mutable: true }),
); );
break; break;
case "LocalStore": }
if (!this.localMap.has(k.target)) { case "Load": {
const source = this.regs.get(k.source);
if (!source || source.kind.tag !== "LocalPtr") {
throw new Error(); throw new Error();
} }
this.locals[this.localMap.get(k.target)!] = this.regs.get( const value = this.locals[source.kind.localIdx];
k.source, if (!value) {
)!; throw new Error();
}
this.regs.set(inst, value);
break; break;
}
case "Store": {
const target = this.regs.get(k.target)!;
if (target.kind.tag !== "LocalPtr") {
throw new Error();
}
const source = this.regs.get(k.source)!;
this.locals[target.kind.localIdx] = source;
break;
}
case "Jump": { case "Jump": {
this.bb = k.target; this.bb = k.target;
this.instIdx = 0; this.instIdx = 0;
@ -207,6 +212,8 @@ class Val {
case "Int": case "Int":
case "Bool": case "Bool":
return `${k.value}`; return `${k.value}`;
case "LocalPtr":
return `<pointer>`;
case "Fn": case "Fn":
return `<${k.fn.ty.pretty()}>`; return `<${k.fn.ty.pretty()}>`;
default: default:
@ -219,4 +226,5 @@ type ValKind =
| { tag: "Void" } | { tag: "Void" }
| { tag: "Int"; value: number } | { tag: "Int"; value: number }
| { tag: "Bool"; value: boolean } | { tag: "Bool"; value: boolean }
| { tag: "LocalPtr"; mutable: boolean; localIdx: number }
| { tag: "Fn"; fn: mir.Fn }; | { tag: "Fn"; fn: mir.Fn };

View File

@ -102,6 +102,12 @@ export class Ty {
if (this.is("Bool")) { if (this.is("Bool")) {
return "bool"; return "bool";
} }
if (this.is("Ptr")) {
return `*${this.kind.ty.pretty()}`;
}
if (this.is("PtrMut")) {
return `*mut ${this.kind.ty.pretty()}`;
}
if (this.is("Fn")) { if (this.is("Fn")) {
return `fn (${ return `fn (${
this.kind.params.map((param) => param.pretty()).join(", ") this.kind.params.map((param) => param.pretty()).join(", ")
@ -124,5 +130,7 @@ export type TyKind =
| { tag: "Void" } | { tag: "Void" }
| { tag: "Int" } | { tag: "Int" }
| { tag: "Bool" } | { tag: "Bool" }
| { tag: "Ptr"; ty: Ty }
| { tag: "PtrMut"; ty: Ty }
| { tag: "Fn"; params: Ty[]; retTy: Ty } | { tag: "Fn"; params: Ty[]; retTy: Ty }
| { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> }; | { tag: "FnStmt"; ty: Ty; stmt: ast.NodeWithKind<"FnStmt"> };

23
tests/pointer.ethlang Normal file
View File

@ -0,0 +1,23 @@
fn main()
{
let a = 1;
let b: *int = &a;
// expect: 1
print_int(*b);
a = 2;
// expect: 2
print_int(*b);
let c: *mut int = &mut a;
*c = 3;
// expect: 3
print_int(a);
// expect: 3
print_int(*c);
}

View File

@ -29,7 +29,7 @@ run_test_file() {
then then
if grep -q '// expect:' $file if grep -q '// expect:' $file
then then
expected=$(grep '// expect:' $file | sed -E 's/\/\/ expect: (.*?)/\1/g') expected=$(grep '// expect:' $file | sed -E 's/\s*\/\/\s+expect: (.*?)/\1/g')
if [[ $output != $expected ]] if [[ $output != $expected ]]
then then
echo "-- failed: incorrect output --" echo "-- failed: incorrect output --"