mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Store assignment expressions into module lets declared without an initializer
A module `let` declared without an initializer whose type uses its null pointer as the TDZ sentinel (a function, for example) is initialized when its declaration runs, while its slot is still empty. markUninitializedWrites marked the assignment statements that run after the declaration as initializing, but not assignments in expression position. Those ran the pointer check and threw "ReferenceError: Cannot access 'g' before initialization" where Node stores the value, so the deferred-resolver pattern `let resolve!: ...; new Promise((r) => (resolve = r))` failed, as did `(h) => (g = h)` and `const r = (g = h)`. An assignment expression to such a binding now becomes an initializing `assign` whose value a temp holds and yields. Writes that can run before the declaration keep the flag check, so they still throw Node's ReferenceError.
This commit is contained in:
@@ -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<string>();
|
||||
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(
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7109-uninitialized-let-assignment-expressions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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<number>((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());
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3300, "san": 4500 }
|
||||
Reference in New Issue
Block a user