mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Merge pull request #828 from vercel-labs/fix/lowerer-init-order
Keep null distinct from the TDZ in forward-captured nullable bindings
This commit is contained in:
@@ -4648,18 +4648,37 @@ export class LlEmitter {
|
||||
B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`);
|
||||
}
|
||||
|
||||
/** The payload a TDZ box holds before its declaration runs, when that is
|
||||
* not NULL. A nullable union's value is its arm pointer with NULL as a
|
||||
* unit arm (nullable-unions.ts), so a stored `null`/`undefined` would
|
||||
* read back as the temporal dead zone; such boxes start at the module's
|
||||
* immortal NULLABLE_ABSENT sentinel instead, which no value of the
|
||||
* binding can hold and every RC entry point and the collector skip. */
|
||||
private tdzEmptySentinel(t: IrType): string | null {
|
||||
if (!this.nullableUnions.of(t)) return null;
|
||||
this.needsNullableAbsent = true;
|
||||
this.immortalValues.add(NULLABLE_ABSENT);
|
||||
return NULLABLE_ABSENT;
|
||||
}
|
||||
|
||||
/** The TDZ-guarded read of a boxed binding: an empty payload slot is
|
||||
* 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 checkTdz(box: string, name: string): string {
|
||||
private checkTdz(box: string, t: IrType, name: string): string {
|
||||
const B = this.B;
|
||||
const slotp = B.tmp();
|
||||
const slotv = B.tmp();
|
||||
const empty = B.tmp();
|
||||
B.line(`${slotp} = getelementptr inbounds %ScrBox, ptr ${box}, i64 0, i32 5`);
|
||||
B.line(`${slotv} = load i64, ptr ${slotp}`);
|
||||
B.line(`${empty} = icmp eq i64 ${slotv}, 0`);
|
||||
const sentinel = this.tdzEmptySentinel(t);
|
||||
if (sentinel !== null) {
|
||||
B.line(`${slotv} = load ptr, ptr ${slotp}`);
|
||||
B.line(`${empty} = icmp eq ptr ${slotv}, ${sentinel}`);
|
||||
} else {
|
||||
B.line(`${slotv} = load i64, ptr ${slotp}`);
|
||||
B.line(`${empty} = icmp eq i64 ${slotv}, 0`);
|
||||
}
|
||||
this.throwIfUninitialized(empty, name);
|
||||
return slotv;
|
||||
}
|
||||
@@ -4699,7 +4718,7 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
tdzBoxRead(box: string, t: IrType, name: string): string {
|
||||
const slotv = this.checkTdz(box, name);
|
||||
const slotv = this.checkTdz(box, t, name);
|
||||
const B = this.B;
|
||||
const acc = boxAccess(t);
|
||||
if (acc === "ref") return this.boxGet(box, t);
|
||||
@@ -4727,7 +4746,7 @@ export class LlEmitter {
|
||||
): void {
|
||||
if (local.tdz) {
|
||||
const first = initializes || !local.mutable;
|
||||
const slotv = first ? null : this.checkTdz(box, local.name);
|
||||
const slotv = first ? null : this.checkTdz(box, local.type, local.name);
|
||||
const acc = boxAccess(local.type);
|
||||
if (acc !== "ref") {
|
||||
if (first) this.tdzScalarInit(box, local.type, value);
|
||||
@@ -5447,7 +5466,13 @@ export class LlEmitter {
|
||||
B.line(`${box} = ${boxNew} ; let ${b.local!.name} (boxed)`);
|
||||
B.line(`store ptr ${box}, ptr ${b.slot}`);
|
||||
this.scopes[this.scopes.length - 1]!.push({ slot: b.slot, type: b.type, boxed: true });
|
||||
if (s.init === null) break;
|
||||
if (s.init === null) {
|
||||
// An empty TDZ box whose payload can legitimately be NULL
|
||||
// starts at its distinct sentinel (tdzEmptySentinel).
|
||||
const sentinel = b.local!.tdz === true ? this.tdzEmptySentinel(b.type) : null;
|
||||
if (sentinel !== null) B.line(`store ptr ${sentinel}, ptr ${this.boxSlot(box)}`);
|
||||
break;
|
||||
}
|
||||
const v = this.emitExpr(s.init);
|
||||
if (isRefCounted(v.type)) this.moveTemp(v); // the box takes ownership
|
||||
if (b.local!.tdz === true && boxAccess(b.type) !== "ref") {
|
||||
|
||||
@@ -1592,7 +1592,10 @@ export interface IrLocal {
|
||||
* empty throws JS's catchable ReferenceError ("Cannot access 'name'
|
||||
* before initialization"), exactly Node's temporal dead zone. Always
|
||||
* paired with `boxed`; scalar payloads use a one-element array cell so
|
||||
* the NULL slot remains the TDZ sentinel. Capture entries inherit the flag. */
|
||||
* the NULL slot remains the TDZ sentinel. Backends whose values of the
|
||||
* binding's type can themselves be NULL (the LLVM backend's nullable
|
||||
* unions, where NULL is `null`/`undefined`) mark the empty box with a
|
||||
* distinct sentinel instead. Capture entries inherit the flag. */
|
||||
tdz?: true;
|
||||
}
|
||||
|
||||
|
||||
@@ -15736,6 +15736,12 @@
|
||||
"<repo>/tests/corpus/4597-callback-field-method-name.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4596-tdz-nullable-forward-capture.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4596-tdz-nullable-forward-capture.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
export {};
|
||||
|
||||
// A closure created above a `let`/`const` it captures holds the binding in
|
||||
// its temporal dead zone until the declaration runs. A binding of a
|
||||
// reference type joined with `null` or `undefined` may legitimately hold
|
||||
// that unit value afterwards; it must read as the value, not as the TDZ.
|
||||
|
||||
function attempt(label: string, action: () => void): void {
|
||||
try {
|
||||
action();
|
||||
console.log(label, "ok");
|
||||
} catch (error) {
|
||||
if (error instanceof Error) console.log(label, error.name, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
class Node2 {
|
||||
readonly name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// The lowering shape that tripped the self-hosted compiler: a host object
|
||||
// whose callback memoizes into later-declared bindings.
|
||||
interface Host {
|
||||
keys(): Set<string>;
|
||||
}
|
||||
class Owner {
|
||||
private readonly host: Host;
|
||||
constructor(host: Host) {
|
||||
this.host = host;
|
||||
}
|
||||
count(): number {
|
||||
return this.host.keys().size;
|
||||
}
|
||||
}
|
||||
function memoized(): void {
|
||||
let computed = 0;
|
||||
const owner = new Owner({ keys: () => (cache ??= compute()) });
|
||||
let cache: Set<string> | null = null;
|
||||
const compute = (): Set<string> => {
|
||||
computed++;
|
||||
return new Set(["a", "b"]);
|
||||
};
|
||||
console.log("memoized", owner.count(), owner.count(), computed);
|
||||
}
|
||||
memoized();
|
||||
|
||||
function setArm(): void {
|
||||
const read = (): string => (value === null ? "null" : `size ${value.size}`);
|
||||
const write = (): void => {
|
||||
value = new Set([1, 2, 3]);
|
||||
};
|
||||
const clear = (): void => {
|
||||
value = null;
|
||||
};
|
||||
attempt("set read before", () => console.log(read()));
|
||||
attempt("set write before", write);
|
||||
let value: Set<number> | null = null;
|
||||
console.log("set initial", read());
|
||||
write();
|
||||
console.log("set written", read());
|
||||
clear();
|
||||
console.log("set cleared", read());
|
||||
}
|
||||
setArm();
|
||||
|
||||
function stringArm(): void {
|
||||
const fill = (): string => (value ??= "filled");
|
||||
const read = (): string => String(value);
|
||||
attempt("string fill before", () => console.log(fill()));
|
||||
let value: string | undefined = undefined;
|
||||
console.log("string initial", read());
|
||||
console.log("string fill", fill(), fill());
|
||||
value = "";
|
||||
console.log("string empty or", value || "fallback", JSON.stringify(read()));
|
||||
value ||= "or-assigned";
|
||||
console.log("string or-assign", read());
|
||||
}
|
||||
stringArm();
|
||||
|
||||
function arrayArm(): void {
|
||||
const push = (n: number): number => (value ??= []).push(n);
|
||||
const length = (): number => value?.length ?? -1;
|
||||
const joined = (): string => value?.join(",") ?? "none";
|
||||
attempt("array length before", () => console.log(length()));
|
||||
let value: number[] | undefined = undefined;
|
||||
console.log("array initial", length(), joined());
|
||||
push(4);
|
||||
push(5);
|
||||
console.log("array pushed", length(), joined());
|
||||
value = undefined;
|
||||
console.log("array reset", length());
|
||||
}
|
||||
arrayArm();
|
||||
|
||||
function mapArm(): void {
|
||||
const get = (k: string): string => value?.get(k) ?? "missing";
|
||||
const ensure = (): Map<string, string> => (value ??= new Map([["k", "v"]]));
|
||||
let value: Map<string, string> | null = null;
|
||||
console.log("map initial", get("k"));
|
||||
ensure().set("j", "w");
|
||||
console.log("map ensured", get("k"), get("j"), ensure().size);
|
||||
}
|
||||
mapArm();
|
||||
|
||||
function classArm(): void {
|
||||
const name = (): string => value?.name ?? "none";
|
||||
const swap = (next: Node2 | null): string => {
|
||||
const before = name();
|
||||
value = next;
|
||||
return `${before}->${name()}`;
|
||||
};
|
||||
attempt("class swap before", () => console.log(swap(new Node2("early"))));
|
||||
let value: Node2 | null = null;
|
||||
console.log("class", swap(new Node2("first")), swap(null), swap(new Node2("second")));
|
||||
}
|
||||
classArm();
|
||||
|
||||
function recordArm(): void {
|
||||
const label = (): string => (value === undefined ? "undefined" : value.label);
|
||||
const point = { label: "p", x: 1 };
|
||||
let value: { label: string; x: number } | undefined = undefined;
|
||||
console.log("record initial", label());
|
||||
value = point;
|
||||
console.log("record set", label(), value.x);
|
||||
}
|
||||
recordArm();
|
||||
|
||||
// `null` and `undefined` are distinct values of the same binding.
|
||||
function bothUnits(): void {
|
||||
const describe = (): string =>
|
||||
value === undefined ? "undefined" : value === null ? "null" : value.name;
|
||||
const set = (next: Node2 | null | undefined): void => {
|
||||
value = next;
|
||||
};
|
||||
attempt("both read before", () => console.log(describe()));
|
||||
let value: Node2 | null | undefined = undefined;
|
||||
console.log("both", describe());
|
||||
set(null);
|
||||
console.log("both", describe());
|
||||
set(new Node2("present"));
|
||||
console.log("both", describe());
|
||||
set(undefined);
|
||||
console.log("both", describe());
|
||||
}
|
||||
bothUnits();
|
||||
|
||||
function constBinding(): void {
|
||||
const read = (): string => (value === null ? "const null" : value.join("+"));
|
||||
attempt("const read before", () => console.log(read()));
|
||||
const value: string[] | null = Math.random() < 2 ? null : ["never"];
|
||||
console.log(read());
|
||||
}
|
||||
constBinding();
|
||||
|
||||
// Every iteration has its own binding and its own temporal dead zone.
|
||||
function loop(): void {
|
||||
const readers: (() => string)[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const reader = (): string => (value === null ? `null@${i}` : value);
|
||||
readers.push(reader);
|
||||
if (i === 1) attempt("loop read before", () => console.log(reader()));
|
||||
let value: string | null = i === 2 ? "two" : null;
|
||||
}
|
||||
console.log("loop", readers.map((reader) => reader()).join(" "));
|
||||
}
|
||||
loop();
|
||||
|
||||
// A forward-captured binding inside a reference cycle (closure -> box ->
|
||||
// instance -> closure) is reclaimed without touching the empty sentinel.
|
||||
class Holder {
|
||||
callback: () => string = () => "unset";
|
||||
}
|
||||
function cycle(): string {
|
||||
const holder = new Holder();
|
||||
holder.callback = () =>
|
||||
value === null ? "cycle null" : value.callback === holder.callback ? "cycle self" : "cycle other";
|
||||
let value: Holder | null = null;
|
||||
const first = holder.callback();
|
||||
value = holder;
|
||||
return `${first}, ${holder.callback()}`;
|
||||
}
|
||||
for (let i = 0; i < 3; i++) console.log(cycle());
|
||||
|
||||
// The empty box is released when its scope unwinds before initialization;
|
||||
// a closure that escaped the frame still sees the temporal dead zone.
|
||||
let escapedRead: () => number = () => -1;
|
||||
function abandoned(): void {
|
||||
escapedRead = () => value?.length ?? 0;
|
||||
if (Math.random() < 2) throw new Error("unwound before declaration");
|
||||
let value: number[] | null = null;
|
||||
console.log("abandoned unreachable", escapedRead());
|
||||
}
|
||||
attempt("abandoned", abandoned);
|
||||
attempt("escaped read", () => console.log(escapedRead()));
|
||||
Reference in New Issue
Block a user