diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 092588a6e..4b1e74fcf 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -463,16 +463,38 @@ export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void /** Writes to a binding declared without an initializer that run after its * declaration store into an initialized binding whose slot may still be * empty: the pointer guard must not refuse them. Writes that can run - * earlier carry the flag check rewriteFunction inserted. */ + * earlier carry the flag check rewriteFunction inserted. An assignment in + * expression position (`(f = v)`) becomes an initializing `assign` whose + * value a temp holds and yields. */ function markUninitializedWrites(lowerer: Lowerer, mod: IrModule): void { const ids = new Set(); for (const [id, flag] of lowerer.cycleInitFlags) if (flag.uninitialized) ids.add(id); if (ids.size === 0) return; const mark = (s: IrStmt): IrStmt => s.kind === "assign" && !s.initializes && ids.has(s.localId) ? { ...s, initializes: true } : s; - const rewriteExpr = (e: IrExpr): IrExpr => mapExprChildren(e, rewriteExpr, rewriteStmt); - const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); - for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); + for (const fn of mod.functions) { + let temps = 0; + const markExpr = (e: IrExpr): IrExpr => { + if (e.kind !== "assignExpr" || !ids.has(e.localId)) return e; + const t = `%uninitWrite.${temps++}`; + fn.locals.push({ id: t, name: "%uninitWrite", type: e.value.type, mutable: false }); + const value: IrExpr = { kind: "varRef", localId: t, type: e.value.type, loc: e.loc }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: t, init: e.value, loc: e.loc }, + { kind: "assign", localId: e.localId, value, initializes: true, loc: e.loc }, + ], + result: value, + type: e.type, + loc: e.loc, + }; + }; + const rewriteExpr = (e: IrExpr): IrExpr => + markExpr(mapExprChildren(e, rewriteExpr, rewriteStmt)); + const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); + fn.body = fn.body.map(rewriteStmt); + } } function rewriteFunction( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json new file mode 100644 index 000000000..47973fee5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7109-uninitialized-let-assignment-expressions.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7109-uninitialized-let-assignment-expressions.ts b/tests/corpus/7109-uninitialized-let-assignment-expressions.ts new file mode 100644 index 000000000..1354ab100 --- /dev/null +++ b/tests/corpus/7109-uninitialized-let-assignment-expressions.ts @@ -0,0 +1,63 @@ +// A module `let` declared without an initializer is initialized (to +// undefined) when its declaration runs. Assignments in expression +// position store into it like assignment statements do. + +// The deferred-resolver pattern: an expression-bodied executor. +let resolve!: (value: number) => void; +const pending = new Promise((r) => (resolve = r)); +resolve(1); +console.log("resolved", await pending); + +// An expression-bodied setter, and the assignment's value. +let handler!: (n: number) => string; +const setHandler = (h: (n: number) => string) => (handler = h); +const returned = setHandler((n) => `handled ${n}`); +console.log(returned(2), handler(3)); + +// The value held in a binding first. +let next!: () => string; +const install = (f: () => string) => { + const r = (next = f); + return r; +}; +console.log(install(() => "installed")(), next()); + +// Chained, conditional and argument positions. +let first!: (n: number) => number; +let second!: (n: number) => number; +first = second = (n) => n + 1; +console.log("chained", first(1), second(2)); +let picked!: () => string; +const choose = (flag: boolean) => (flag ? (picked = () => "yes") : (picked = () => "no")); +choose(false); +console.log("conditional", picked()); +const call = (f: () => string) => f(); +let passed!: () => string; +console.log("argument", call((passed = () => "passed")), passed()); + +// Reassignment releases the earlier function and keeps the new one. +let counter!: () => number; +const make = (start: number) => { + let n = start; + return (counter = () => ++n); +}; +make(10); +make(20); +console.log("reassigned", counter(), counter()); + +// An optional binding behaves the same way. +let maybe: ((s: string) => string) | undefined; +const setMaybe = (f: (s: string) => string) => (maybe = f); +console.log("optional", setMaybe((s) => s.toUpperCase())("a"), maybe?.("b")); + +// A write that runs before the declaration still throws. +function early(): () => number { + return (late = () => 1); +} +try { + early(); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +let late!: () => number; +console.log("after declaration", early()(), late()); diff --git a/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json b/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json new file mode 100644 index 000000000..46c7f7832 --- /dev/null +++ b/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4500 }