Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/compiler/src/backend/c/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { CEmitter } from "./c-emitter.js";
import type { IrLocal } from "../../ir/ir.js";
import { mangleLocal } from "../mangle.js";
import { boxAccess, cType } from "./types.js";

/** The empty payload slot is the TDZ sentinel for both references and
* scalar cells. Check before reading, or after evaluating a write's RHS. */
export function checkTdz(emitter: CEmitter, local: IrLocal): void {
const box = mangleLocal(local.id);
const errName = emitter.internLiteral("ReferenceError");
const message = emitter.internLiteral(`Cannot access '${local.name}' before initialization`);
emitter.line(`if (${box}->slot == 0) { /* temporal dead zone */`);
emitter.indent++;
emitter.line(`scr_throw_error_named((ScrStr *)&${errName}, (ScrStr *)&${message});`);
emitter.emitUnwind();
emitter.indent--;
emitter.line("}");
}

/** Read a shared binding. Reference results own a retain; scalar cells
* are borrowed from their live box and return a copied value. */
export function readBox(emitter: CEmitter, local: IrLocal): string {
const box = mangleLocal(local.id);
const acc = boxAccess(local.type);
if (local.tdz) checkTdz(emitter, local);
if (acc === "ref") return `(${cType(local.type).trim()})scr_box_get_ref(${box})`;
return local.tdz
? `scr_arr_get_${acc}((ScrArr *)(uintptr_t)${box}->slot, 0)`
: `scr_box_get_${acc}(${box})`;
}

/** Store an owned payload. A mutable scalar TDZ box keeps the same cell
* after initialization, so every closure sees subsequent assignments. */
export function writeBox(emitter: CEmitter, local: IrLocal, value: string, initializes = false): void {
const box = mangleLocal(local.id);
const acc = boxAccess(local.type);
if (local.tdz) {
// Historical IR represents a const's declaration with plain assign.
// Such bindings have no legal subsequent assignment; keep it readable.
const first = initializes || !local.mutable;
if (!first) checkTdz(emitter, local);
if (acc !== "ref") {
if (first) {
const cell = `sc_t${emitter.tempCounter++}`;
emitter.line(`ScrArr *${cell} = ${emitter.arrNewC(local.type, 1)};`);
emitter.line(`scr_arr_push_${acc}(${cell}, ${value});`);
emitter.line(`scr_box_set_ref(${box}, ${cell});`);
} else {
emitter.line(`scr_arr_set_${acc}((ScrArr *)(uintptr_t)${box}->slot, 0, ${value});`);
}
return;
}
}
emitter.line(`scr_box_set_${acc}(${box}, ${value});`);
}
48 changes: 13 additions & 35 deletions packages/compiler/src/backend/c/exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { arrayOf, BOOL, BYTES_U8, bytesOf, canMarshalFuncIntoIsland, CHILDSTREAM
import { boxAccess, BYTES_NUM_KIND_C, BYTES_NUM_VAR_C, bytesElemKindC, cDecl, cFnPtrCast, cNumberLiteral, cStringLiteral, cType, DV_GET_KIND_C, DV_SET_KIND_C, elemAccess, mapKeyAccess, mapKeyKindC, mapValKindC, releaseCallC, retainCallC, vAdapters } from "./types.js";
import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleField, mangleFnClosure, mangleFunction, mangleGlobal, mangleLocal, mangleRecordClone, mangleRecordNew, mangleRecordStruct, mangleVtStruct } from "../mangle.js";
import { OVERFLOW_MEMBER } from "./shapes.js";
import { readBox, writeBox } from "./bindings.js";
import { dynDestrCheckHelper, dynIterNHelper, dynKeyGetHelper, unionWidenHelper } from "./walkers.js";
import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js";
import { genResultThunkFor } from "./async.js";
Expand Down Expand Up @@ -798,35 +799,7 @@ function emitLiteralExpr(
}
const name = mangleLocal(e.localId);
if (local?.boxed) {
// Reads go through the shared binding; ref kinds come out +1.
const acc = boxAccess(e.type);
// A scalar TDZ box stores its value in a one-element ARRAY cell
// (the raw scalar slot has no spare sentinel state): reads peek
// the cell through the slot — the box keeps the array alive, so
// no retain/release pair is needed for the copied-out scalar.
const read =
acc === "ref"
? `(${cType(e.type).trim()})scr_box_get_ref(${name})`
: local?.tdz
? `scr_arr_get_${acc}((ScrArr *)(uintptr_t)${name}->slot, 0)`
: `scr_box_get_${acc}(${name})`;
if (local.tdz) {
// Forward-captured const: an empty box is the temporal dead
// zone — throw Node's exact catchable ReferenceError. The test
// peeks the payload slot BEFORE the retaining read (get_ref on
// an empty box would dereference NULL; the scalar cell peek
// would too). Interned literals are immortal (rc SIZE_MAX), so
// handing them to the ownership-taking thrower is safe.
const errName = emitter.internLiteral("ReferenceError");
const msg = emitter.internLiteral(`Cannot access '${local.name}' before initialization`);
emitter.line(`if (${name}->slot == 0) { /* TDZ: read before initialization */`);
emitter.indent++;
emitter.line(`scr_throw_error_named((ScrStr *)&${errName}, (ScrStr *)&${msg});`);
emitter.emitUnwind();
emitter.indent--;
emitter.line(`}`);
}
return emitter.newTemp(e.type, read);
return emitter.newTemp(e.type, readBox(emitter, local));
}
return emitter.newTemp(e.type, isRefCounted(e.type) ? retainCallC(e.type, name) : name);
}
Expand Down Expand Up @@ -912,14 +885,13 @@ function emitOperatorExpr(
const local = emitter.currentLocals.get(e.localId);
const one = e.op === "+" ? "+ 1" : "- 1";
if (local?.boxed) {
const box = mangleLocal(e.localId);
const old = emitter.newTemp(e.type, `scr_box_get_${boxAccess(e.type)}(${box})`);
const old = emitter.newTemp(e.type, readBox(emitter, local));
if (e.prefix) {
const t = emitter.newTemp(e.type, `${old.name} ${one}`);
emitter.line(`scr_box_set_${boxAccess(e.type)}(${box}, ${t.name});`);
writeBox(emitter, local, t.name);
return t;
}
emitter.line(`scr_box_set_${boxAccess(e.type)}(${box}, ${old.name} ${one});`);
writeBox(emitter, local, `${old.name} ${one}`);
return old;
}
if (!local && !emitter.globalsById.has(e.localId)) {
Expand Down Expand Up @@ -1003,7 +975,7 @@ function emitOperatorExpr(
// box_set takes ownership of the passed reference, so hand it a
// retained copy and keep the temp's own reference for the yield.
const stored = isRefCounted(v.type) ? retainCallC(v.type, v.name) : v.name;
emitter.line(`scr_box_set_${boxAccess(local.type)}(${mangleLocal(e.localId)}, ${stored});`);
writeBox(emitter, local, stored);
return v;
}
if (!local && !emitter.globalsById.has(e.localId)) {
Expand Down Expand Up @@ -2739,7 +2711,7 @@ function emitCallExpr(

function emitRecordExpr(
emitter: CEmitter,
e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys">,
e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys" | "recordOvfHas">,
): Temp {
switch (e.kind) {
case "fieldGet":
Expand Down Expand Up @@ -2819,6 +2791,11 @@ function emitRecordExpr(
const helper = emitter.recordKeyGetHelper(e.shapeId, e.type, e.overflowOnly === true);
return emitter.newTemp(e.type, `${helper}(${obj.name}, ${key.name})`);
}
case "recordOvfHas": {
const obj = emitter.emitExpr(e.obj);
const key = emitter.emitExpr(e.key);
return emitter.newTemp(e.type, `scr_map_has_str(${obj.name}->${OVERFLOW_MEMBER}, ${key.name})`);
}
case "recordOvfKeys": {
// The overflow map's live keys in JS own-key order — a fresh
// string[] snapshot (+1); the record is borrowed.
Expand Down Expand Up @@ -9206,6 +9183,7 @@ export function emitExpr(emitter: CEmitter, e: IrExpr): Temp {
case "recordClone":
case "recordKeyGet":
case "recordOvfKeys":
case "recordOvfHas":
return emitRecordExpr(emitter, e);
case "dynFrom":
case "dynFromJsval":
Expand Down
64 changes: 64 additions & 0 deletions packages/compiler/src/backend/c/may-throw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { expect, test } from "vitest";
import { F64, VOID, funcOf, type IrExpr, type IrFunction, type IrLocal, type IrModule, type IrStmt } from "../../ir/ir.js";
import { computeMayThrow } from "./may-throw.js";

const loc = { file: "tdz.ts", start: 0, end: 1 };
const value: IrExpr = { kind: "numLit", value: 1, type: F64, loc };
const local: IrLocal = { id: "value", name: "value", type: F64, mutable: true, boxed: true, tdz: true };
function fn(name: string, body: IrStmt[], locals: IrLocal[] = [local]): IrFunction {
return { name, body, locals, params: [], returnType: VOID, loc };
}
function moduleWith(...functions: IrFunction[]): IrModule {
return { irVersion: 11, sourceFile: loc.file, entry: "caller", functions };
}
const assignment: IrStmt = { kind: "assign", localId: local.id, value, loc };
const expression: IrExpr = { kind: "assignExpr", localId: local.id, value, type: F64, loc };
const increment: IrExpr = { kind: "incDec", localId: local.id, op: "+", prefix: false, type: F64, loc };
const read: IrExpr = { kind: "varRef", localId: local.id, type: F64, loc };
const exprStmt = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });

test.each([
["store", assignment],
["assignment expression", exprStmt(expression)],
["increment", exprStmt(increment)],
["read", exprStmt(read)],
] as const)("TDZ %s propagates through the direct call graph", (_name, operation) => {
const target = fn("target", [operation]);
const middle = fn("middle", [exprStmt({ kind: "call", callee: "target", args: [], type: VOID, loc })], []);
const caller = fn("caller", [exprStmt({ kind: "call", callee: "middle", args: [], type: VOID, loc })], []);
const answer = computeMayThrow(moduleWith(caller, middle, target));
expect([...answer.fns].sort()).toEqual(["caller", "middle", "target"]);
expect(answer.indirect).toBe(false);
});

test("TDZ stores seed indirect-call propagation", () => {
const closure: IrExpr = { kind: "closure", fnName: "target", captures: [], type: funcOf([], VOID), loc };
const caller = fn("caller", [exprStmt({ kind: "callValue", callee: closure, args: [], type: VOID, loc })], []);
const answer = computeMayThrow(moduleWith(caller, fn("target", [assignment])));
expect(answer.indirect).toBe(true);
expect([...answer.fns].sort()).toEqual(["caller", "target"]);
});

test.each([true, false])("declaration stores do not throw solely for TDZ (mutable=%s)", (mutable) => {
const initialize: IrStmt = { ...assignment, initializes: true };
expect(computeMayThrow(moduleWith(fn("caller", [initialize], [{ ...local, mutable }]))).fns.size).toBe(0);
});

test("legacy const TDZ stores remain initialization", () => {
const immutable = { ...local, mutable: false };
expect(computeMayThrow(moduleWith(fn("caller", [assignment], [immutable]))).fns.size).toBe(0);
});

test("ordinary boxed stores do not gain an exception edge", () => {
const ordinary: IrLocal = { id: local.id, name: local.name, type: F64, mutable: true, boxed: true };
expect(computeMayThrow(moduleWith(fn("caller", [assignment, exprStmt(expression), exprStmt(increment)], [ordinary]))).fns.size).toBe(0);
});

test("initializers still propagate exceptions from their right-hand side", () => {
const initialize: IrStmt = {
...assignment, initializes: true,
value: { kind: "call", callee: "failure", args: [], type: F64, loc },
};
const failure = fn("failure", [{ kind: "throw", value, loc }], []);
expect([...computeMayThrow(moduleWith(fn("caller", [initialize]), failure)).fns].sort()).toEqual(["caller", "failure"]);
});
15 changes: 12 additions & 3 deletions packages/compiler/src/backend/c/may-throw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ export function computeMayThrow(mod: IrModule): { fns: Set<string>; indirect: bo
}
for (const fn of mod.functions) {
const f: Facts = { throws: false, callees: [], callsValue: false };
// TDZ locals (forward-captured consts): every read tests the box and
// throws the catchable ReferenceError while it is empty.
// TDZ reads and non-initializing writes can throw ReferenceError.
// Capture locals carry the same flag as the declaring binding.
const tdzIds = new Set(fn.locals.filter((l) => l.tdz).map((l) => l.id));
const mutableTdzIds = new Set(fn.locals.filter((l) => l.tdz && l.mutable).map((l) => l.id));
// The IR is plain JSON: a generic walk keyed on `kind` stays correct as
// nodes grow fields (types' own `kind`s never collide with these).
const visit = (node: unknown): void => {
Expand All @@ -67,7 +68,15 @@ export function computeMayThrow(mod: IrModule): { fns: Set<string>; indirect: bo
f.throws = true;
break;
case "varRef":
if (tdzIds.size > 0 && tdzIds.has(rec["localId"] as string)) f.throws = true;
case "incDec":
case "assignExpr":
if (tdzIds.has(rec["localId"] as string)) f.throws = true;
break;
case "assign":
// A declaration is allowed to fill an empty box. Legacy const
// TDZ stores also initialize; only mutable subsequent stores
// introduce the new write-side exception edge.
if (rec["initializes"] !== true && mutableTdzIds.has(rec["localId"] as string)) f.throws = true;
break;
case "dynCheck":
case "caughtCheck":
Expand Down
17 changes: 4 additions & 13 deletions packages/compiler/src/backend/c/stmts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mangleField, mangleGlobal, mangleLocal, mangleRawParam } from "../mangl
import { BOOL, CAUGHT, IrExpr, IrStmt, RUNTIME_ERROR_CLASSES, isRefCounted } from "../../ir/ir.js";
import { boxAccess, cDecl, cStringLiteral, elemAccess, vAdapters } from "./types.js";
import { OVERFLOW_MEMBER } from "./shapes.js";
import { writeBox } from "./bindings.js";
import { emitStableReceiver } from "./exprs.js";
import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js";
import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
Expand Down Expand Up @@ -280,19 +281,9 @@ function emitStmtBody(emitter: CEmitter, s: IrStmt): void {
const target = mangleLocal(s.localId);
const v = emitter.emitExpr(s.value);
if (local!.boxed) {
// A scalar TDZ box (forward-captured const): the initializing
// write mints the one-element array cell — set_ref moves it in
// (and the empty-slot sentinel ends here).
if (local!.tdz && boxAccess(local!.type) !== "ref") {
const acc = boxAccess(local!.type);
const cell = `sc_t${emitter.tempCounter++}`;
emitter.line(`ScrArr *${cell} = ${emitter.arrNewC(local!.type, 1)};${emitter.srcComment(s.loc)}`);
emitter.line(`scr_arr_push_${acc}(${cell}, ${v.name});`);
emitter.line(`scr_box_set_ref(${target}, ${cell});`);
break;
}
if (isRefCounted(v.type)) emitter.moveTemp(v); // set_ref releases the old value
emitter.line(`scr_box_set_${boxAccess(local!.type)}(${target}, ${v.name});${emitter.srcComment(s.loc)}`);
writeBox(emitter, local, v.name, s.initializes);
// A TDZ failure must still unwind the evaluated RHS.
if (isRefCounted(v.type)) emitter.moveTemp(v);
break;
}
emitter.moveTemp(v);
Expand Down
44 changes: 33 additions & 11 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2857,7 +2857,7 @@ class LlEmitter {
* the temporal dead zone — throw Node's exact catchable ReferenceError
* (exprs.ts's varRef guard). Scalars then peek the one-element
* array cell; ref kinds read the box normally (+1). */
private tdzBoxRead(box: string, t: IrType, name: string): string {
private checkTdz(box: string, name: string): string {
const B = this.B;
const slotp = B.tmp();
const slotv = B.tmp();
Expand All @@ -2877,6 +2877,12 @@ class LlEmitter {
B.line(`call void @scr_throw_error_named(ptr ${errName}, ptr ${msg})`);
this.emitUnwind();
B.startBlock(lk);
return slotv;
}

private tdzBoxRead(box: string, t: IrType, name: string): string {
const slotv = this.checkTdz(box, name);
const B = this.B;
const acc = boxAccess(t);
if (acc === "ref") return this.boxGet(box, t);
// The scalar cell peek: the box keeps the array alive, so no
Expand All @@ -2890,6 +2896,28 @@ class LlEmitter {
return v;
}

/** Declaration stores initialize an empty TDZ box. Later stores check
* it after the RHS and update scalar cells in place. References move in. */
private writeBindingBox(box: string, local: IrLocal, value: string, initializes = false, borrowed = false): void {
if (local.tdz) {
const first = initializes || !local.mutable;
const slotv = first ? null : this.checkTdz(box, local.name);
const acc = boxAccess(local.type);
if (acc !== "ref") {
if (first) this.tdzScalarInit(box, local.type, value);
else {
const cell = this.B.tmp();
this.B.line(`${cell} = inttoptr i64 ${slotv!} to ptr`);
const ty = acc === "bool" ? "i1" : "double";
this.declare(`declare void @scr_arr_set_${acc}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`);
this.B.line(`call void @scr_arr_set_${acc}(ptr ${cell}, double ${f64Lit(0)}, ${ty} ${value})`);
}
return;
}
}
this.boxSet(box, local.type, borrowed && isRefCounted(local.type) ? this.retainValue(value, local.type) : value);
}

// ── functions ───────────────────────────────────────────────────────────

/** The LLVM symbol a direct call or closure enters a function through:
Expand Down Expand Up @@ -3254,15 +3282,9 @@ class LlEmitter {
const b = this.binding(s.localId);
const v = this.emitExpr(s.value);
if (b.kind === "boxed") {
if (isRefCounted(v.type)) this.moveTemp(v); // set_ref releases the old value
// A scalar TDZ box (forward-captured const): the initializing
// write mints the one-element array cell — set_ref moves it in
// (and the empty-slot sentinel ends here).
if (b.local!.tdz === true && boxAccess(b.type) !== "ref") {
this.tdzScalarInit(this.loadBox(b.slot), b.type, v.name);
break;
}
this.boxSet(this.loadBox(b.slot), b.type, v.name);
this.writeBindingBox(this.loadBox(b.slot), b.local!, v.name, s.initializes);
// The RHS remains frame-owned until a possible TDZ throw passes.
if (isRefCounted(v.type)) this.moveTemp(v);
break;
}
this.moveTemp(v);
Expand Down Expand Up @@ -4204,7 +4226,7 @@ class LlEmitter {
return emitCallExpr(this.expressionContext(), e);
}

private emitRecordExpr(e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys">): LlValue {
private emitRecordExpr(e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys" | "recordOvfHas">): LlValue {
return emitRecordExpr(this.expressionContext(), e);
}

Expand Down
Loading
Loading