Reuse reachability lowering output (#178)

* Reuse reachability lowering output

- Retain reachable IR and assemble the emitted module without lowering bodies a second time
- Preserve reached-only artifact filtering, coverage remainder behavior, and add lowering phase timing

* Preserve record order in retained lowering

* Preserve record order in retained helpers

* Preserve generic lowering record order

* Recheck retained generic rest order

* Record retained lowering parity fixtures

* Preserve retained lowering failure parity
This commit is contained in:
Chris Tate
2026-08-20 14:01:39 -05:00
committed by GitHub
parent 5544b23ee9
commit 6310dfb960
13 changed files with 1096 additions and 296 deletions
@@ -1121,6 +1121,7 @@ export function genericFnOf(L: Lowerer, ident: ts.Identifier): GenericFnInfo | n
info.instances.set(key, inst);
L.instantiationQueue.push({ info, inst });
}
L.noteGenericInstanceDemand(inst);
return inst;
}
@@ -6921,50 +6922,8 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression,
helper = `%obj.keys.${L.arrHofHelpers.size}`;
const ref: IrExpr = { kind: "varRef", localId: "r.0", type: argIr, loc };
const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc };
const body: IrStmt[] = [
{ kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: resultT, loc }, loc },
];
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
for (const name of order) {
const f = shape.fields.find((x) => x.name === name)!;
const pushStmt: IrStmt = {
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: outRef,
args: [{ kind: "strLit", value: f.name, type: STRING, loc }],
type: F64,
loc,
},
loc,
};
// Undefined-armed fields: the push is guarded by a tag test (the
// key exists exactly when the arm is not undefined).
const utag = f.type.kind === "union" ? L.armTag(f.type.unionId, UNDEFINED_T) : -1;
body.push(
utag >= 0 && f.type.kind === "union"
? {
kind: "if",
cond: {
kind: "unionIsTag",
unionId: f.type.unionId,
tag: utag,
negated: true,
value: { kind: "recordGet", obj: ref, shapeId: argIr.shapeId, field: f.name, type: f.type, loc },
type: BOOL,
loc,
},
then: [pushStmt],
else_: null,
loc,
}
: pushStmt,
);
}
body.push({ kind: "return", value: outRef, loc });
L.arrHofHelpers.set(key, helper);
L.liftedFns.push({
const fn: IrFunction = {
name: helper,
params: [{ localId: "r.0", name: "r", type: argIr }],
returnType: resultT,
@@ -6972,9 +6931,58 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression,
{ id: "r.0", name: "r", type: argIr, mutable: true },
{ id: "out.0", name: "out", type: resultT, mutable: false },
],
body,
body: [],
loc,
});
};
const finalize = (): void => {
const current = L.shapes.get(argIr.shapeId) ?? shape;
const body: IrStmt[] = [
{ kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: resultT, loc }, loc },
];
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
for (const name of order) {
const f = current.fields.find((x) => x.name === name)!;
const pushStmt: IrStmt = {
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: outRef,
args: [{ kind: "strLit", value: f.name, type: STRING, loc }],
type: F64,
loc,
},
loc,
};
// Undefined-armed fields: the push is guarded by a tag test (the
// key exists exactly when Object.keys would list it).
const utag = f.type.kind === "union" ? L.armTag(f.type.unionId, UNDEFINED_T) : -1;
body.push(
utag >= 0 && f.type.kind === "union"
? {
kind: "if",
cond: {
kind: "unionIsTag",
unionId: f.type.unionId,
tag: utag,
negated: true,
value: { kind: "recordGet", obj: ref, shapeId: argIr.shapeId, field: f.name, type: f.type, loc },
type: BOOL,
loc,
},
then: [pushStmt],
else_: null,
loc,
}
: pushStmt,
);
}
body.push({ kind: "return", value: outRef, loc });
fn.body = body;
};
finalize();
L.shapeOrderHelperFinalizers.push(finalize);
L.liftedFns.push(fn);
}
return { kind: "call", callee: helper, args: [receiver], type: resultT, loc };
}
@@ -7729,119 +7737,15 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression,
valueT = tupleShape.fields.find((f) => f.name === "1")!.type;
}
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`;
let helper = L.arrHofHelpers.get(key);
if (!helper) {
helper = `%obj.${member}.${L.arrHofHelpers.size}`;
const recT = argIr;
const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc };
const body: IrStmt[] = [
{ kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: resultT, loc }, loc },
];
const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc };
for (const name of order) {
const f = shape.fields.find((x) => x.name === name)!;
const raw: IrExpr = { kind: "recordGet", obj: ref, shapeId: argIr.shapeId, field: f.name, type: f.type, loc };
// The pushed element per member; null when the field's value
// cannot flow into the result element type.
const elemOf = (value: IrExpr, vt: IrType): IrExpr | null => {
if (!valueT) return null;
if (typeEquals(vt, valueT)) return value;
if (valueT.kind === "union" && vt.kind !== "union") {
const tag = L.armTag(valueT.unionId, vt);
if (tag >= 0) {
return { kind: "unionWrap", unionId: valueT.unionId, tag, value, type: valueT, loc };
}
}
return null;
};
// Undefined-armed fields: the push is guarded by a tag test, and
// the pushed value is the narrowed non-undefined arm.
let guardUndefTag: number | null = null;
let value: IrExpr = raw;
let vt: IrType = f.type;
if (f.type.kind === "union") {
const undefTag = L.armTag(f.type.unionId, UNDEFINED_T);
if (undefTag >= 0) {
guardUndefTag = undefTag;
const arms = L.unions.get(f.type.unionId)?.arms ?? [];
const others = arms.filter((a) => a.kind !== "undefinedT");
if (typeEquals(f.type, valueT ?? f.type)) {
// The field union IS the result union (single-field shapes):
// push the raw box — but then the undefined skip must NOT
// narrow. Handled below via vt === valueT.
value = raw;
vt = f.type;
} else if (others.length === 1) {
vt = others[0]!;
// A UNIT other arm (`null | undefined` fields — the mixed-
// defaults spread idiom; undefined was filtered above, so
// the unit is null): units carry no payload, so the guarded
// push writes the unit LITERAL — unionNarrow to a unit arm
// (and unionWrap of a narrowed unit) is malformed IR; the
// literal is the one legal unit spelling.
value = isUnitType(vt)
? { kind: "unitLit", unit: "null", type: vt, loc }
: { kind: "unionNarrow", unionId: f.type.unionId, tag: L.armTag(f.type.unionId, vt), value: raw, type: vt, loc };
} else {
L.unsupported(
"SC1090",
call,
`Object.${member} over '${L.fmt(argIr)}' (field '${f.name}' is a multi-arm union that ` +
"cannot re-tag into the result element type — read the fields directly)",
);
}
} else if (!typeEquals(f.type, valueT ?? f.type)) {
L.unsupported(
"SC1090",
call,
`Object.${member} over '${L.fmt(argIr)}' (field '${f.name}' is a union that cannot ` +
"re-tag into the result element type — read the fields directly)",
);
}
}
const coerced = elemOf(value, vt);
if (!coerced) {
L.unsupported(
"SC1090",
call,
`Object.${member} over '${L.fmt(argIr)}' (field '${f.name}' of type '${L.fmt(f.type)}' ` +
`cannot flow into the '${L.fmt(valueT!)}' result element — read the fields directly)`,
);
}
const pushed: IrExpr =
member === "values"
? coerced
: {
kind: "recordLit",
fields: [
{ name: "0", value: { kind: "strLit", value: f.name, type: STRING, loc } },
{ name: "1", value: coerced },
],
type: tupleT!,
loc,
};
const pushStmt: IrStmt = {
kind: "exprStmt",
expr: { kind: "arrIntrinsic", method: "push", receiver: outRef, args: [pushed], type: F64, loc },
loc,
};
body.push(
guardUndefTag !== null && f.type.kind === "union"
? {
kind: "if",
cond: { kind: "unionIsTag", unionId: f.type.unionId, tag: guardUndefTag, negated: true, value: raw, type: BOOL, loc },
then: [pushStmt],
else_: null,
loc,
}
: pushStmt,
);
}
body.push({ kind: "return", value: outRef, loc });
L.arrHofHelpers.set(key, helper);
L.liftedFns.push({
const fn: IrFunction = {
name: helper,
params: [{ localId: "r.0", name: "r", type: recT }],
returnType: resultT,
@@ -7849,9 +7753,120 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression,
{ id: "r.0", name: "r", type: recT, mutable: true },
{ id: "out.0", name: "out", type: resultT, mutable: false },
],
body,
body: [],
loc,
});
};
const finalize = (): void => {
const current = L.shapes.get(argIr.shapeId) ?? shape;
const body: IrStmt[] = [
{ kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: resultT, loc }, loc },
];
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
for (const name of order) {
const f = current.fields.find((x) => x.name === name)!;
const raw: IrExpr = { kind: "recordGet", obj: ref, shapeId: argIr.shapeId, field: f.name, type: f.type, loc };
// The pushed element per member; null when the field's value
// cannot flow into the result element type.
const elemOf = (value: IrExpr, vt: IrType): IrExpr | null => {
if (!valueT) return null;
if (typeEquals(vt, valueT)) return value;
if (valueT.kind === "union" && vt.kind !== "union") {
const tag = L.armTag(valueT.unionId, vt);
if (tag >= 0) {
return { kind: "unionWrap", unionId: valueT.unionId, tag, value, type: valueT, loc };
}
}
return null;
};
// Undefined-armed fields: the push is guarded by a tag test, and
// the pushed value is the narrowed non-undefined arm.
let guardUndefTag: number | null = null;
let value: IrExpr = raw;
let vt: IrType = f.type;
if (f.type.kind === "union") {
const undefTag = L.armTag(f.type.unionId, UNDEFINED_T);
if (undefTag >= 0) {
guardUndefTag = undefTag;
const arms = L.unions.get(f.type.unionId)?.arms ?? [];
const others = arms.filter((a) => a.kind !== "undefinedT");
if (typeEquals(f.type, valueT ?? f.type)) {
// The field union IS the result union (single-field shapes):
// push the raw box — but then the undefined skip must NOT
// narrow. Handled below via vt === valueT.
value = raw;
vt = f.type;
} else if (others.length === 1) {
vt = others[0]!;
// A UNIT other arm (`null | undefined` fields — the mixed-
// defaults spread idiom; undefined was filtered above, so
// the unit is null): units carry no payload, so the guarded
// push writes the unit LITERAL — unionNarrow to a unit arm
// (and unionWrap of a narrowed unit) is malformed IR; the
// literal is the one legal unit spelling.
value = isUnitType(vt)
? { kind: "unitLit", unit: "null", type: vt, loc }
: { kind: "unionNarrow", unionId: f.type.unionId, tag: L.armTag(f.type.unionId, vt), value: raw, type: vt, loc };
} else {
L.unsupported(
"SC1090",
call,
`Object.${member} over '${L.fmt(argIr)}' (field '${f.name}' is a multi-arm union that ` +
"cannot re-tag into the result element type — read the fields directly)",
);
}
} else if (!typeEquals(f.type, valueT ?? f.type)) {
L.unsupported(
"SC1090",
call,
`Object.${member} over '${L.fmt(argIr)}' (field '${f.name}' is a union that cannot ` +
"re-tag into the result element type — read the fields directly)",
);
}
}
const coerced = elemOf(value, vt);
if (!coerced) {
L.unsupported(
"SC1090",
call,
`Object.${member} over '${L.fmt(argIr)}' (field '${f.name}' of type '${L.fmt(f.type)}' ` +
`cannot flow into the '${L.fmt(valueT!)}' result element — read the fields directly)`,
);
}
const pushed: IrExpr =
member === "values"
? coerced
: {
kind: "recordLit",
fields: [
{ name: "0", value: { kind: "strLit", value: f.name, type: STRING, loc } },
{ name: "1", value: coerced },
],
type: tupleT!,
loc,
};
const pushStmt: IrStmt = {
kind: "exprStmt",
expr: { kind: "arrIntrinsic", method: "push", receiver: outRef, args: [pushed], type: F64, loc },
loc,
};
body.push(
guardUndefTag !== null && f.type.kind === "union"
? {
kind: "if",
cond: { kind: "unionIsTag", unionId: f.type.unionId, tag: guardUndefTag, negated: true, value: raw, type: BOOL, loc },
then: [pushStmt],
else_: null,
loc,
}
: pushStmt,
);
}
body.push({ kind: "return", value: outRef, loc });
fn.body = body;
};
finalize();
L.shapeOrderHelperFinalizers.push(finalize);
L.liftedFns.push(fn);
}
return { kind: "call", callee: helper, args: [receiver], type: resultT, loc };
}
@@ -2406,6 +2406,7 @@ export function collectClassShapeInner(L: Lowerer, decl: ts.ClassLikeDeclaration
const key = mapped.map(typeKey).join(",");
const existing = gci.instances.get(key);
if (existing) {
if (existing.info) L.noteGenericClassInstanceDemand(existing.info);
return existing.poisoned ? null : { kind: "object", className: existing.name };
}
// The generic-fn cap, same rationale (polymorphic recursion through
@@ -2444,6 +2445,7 @@ export function collectClassShapeInner(L: Lowerer, decl: ts.ClassLikeDeclaration
}
entry.info = info;
L.genericClassInstances.push(info);
L.noteGenericClassInstanceDemand(info);
L.onLateClassCollected?.(info);
return { kind: "object", className: name };
}
@@ -6349,6 +6349,7 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
const body: IrStmt[] = [
{ kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: resultT, loc }, loc },
];
const fieldStmts = new Map<string, IrStmt>();
// Declared fields, in declaration order. Undefined-valued fields
// skip at runtime (the unset-optional convention); values surface
@@ -6417,7 +6418,7 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
loc,
};
}
body.push(
const fieldStmt: IrStmt =
utag >= 0 && f.type.kind === "union"
? {
kind: "if",
@@ -6426,8 +6427,9 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
else_: null,
loc,
}
: push(pushed),
);
: push(pushed);
fieldStmts.set(f.name, fieldStmt);
body.push(fieldStmt);
}
// The overflow walk: a fresh key snapshot in JS own-key order, each
@@ -6480,8 +6482,9 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
},
{ kind: "return", value: outRef, loc },
);
const suffix = body.slice(1 + fieldStmts.size);
L.arrHofHelpers.set(key, helper);
L.liftedFns.push({
const fn: IrFunction = {
name: helper,
params: [{ localId: "r.0", name: "r", type: argIr }],
returnType: resultT,
@@ -6494,7 +6497,16 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
],
body,
loc,
};
L.shapeOrderHelperFinalizers.push(() => {
const current = L.shapes.get(argIr.shapeId) ?? shape;
const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name);
fn.body = [body[0]!, ...currentOrder.flatMap((name) => {
const stmt = fieldStmts.get(name);
return stmt ? [stmt] : [];
}), ...suffix];
});
L.liftedFns.push(fn);
}
return { kind: "call", callee: helper, args: [receiver], type: resultT, loc };
}
@@ -6876,6 +6888,7 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
loc,
},
];
const fieldStmts = new Map<string, IrStmt>();
// Source declared fields in declaration order, skipping unset
// optionals (stance 37) and the direct-initialized (consumed) names.
for (const ff of orderedFields) {
@@ -6893,7 +6906,7 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
...(to.fields.some((f) => f.name === ff.name) ? {} : { overflowOnly: true as const }),
loc,
};
body.push(
const fieldStmt: IrStmt =
utag >= 0 && ff.type.kind === "union"
? {
kind: "if",
@@ -6902,8 +6915,9 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
else_: null,
loc,
}
: write,
);
: write;
fieldStmts.set(ff.name, fieldStmt);
body.push(fieldStmt);
}
// The source overflow, in JS own-key order (only index-signature
// sources carry one).
@@ -6945,7 +6959,8 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
);
}
body.push({ kind: "return", value: outRef, loc });
L.liftedFns.push({
const suffix = body.slice(1 + fieldStmts.size);
const fn: IrFunction = {
name,
params: [{ localId: "s.0", name: "s", type: fromT }],
returnType: toT,
@@ -6962,7 +6977,16 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
],
body,
loc,
};
L.shapeOrderHelperFinalizers.push(() => {
const current = L.shapes.get(fromId) ?? from;
const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name);
fn.body = [body[0]!, ...currentOrder.flatMap((field) => {
const stmt = fieldStmts.get(field);
return stmt ? [stmt] : [];
}), ...suffix];
});
L.liftedFns.push(fn);
return name;
}
@@ -7050,6 +7074,7 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
const intoSlot = (v: IrExpr, lift: WidthLift | "dyn"): IrExpr =>
lift === "dyn" ? { kind: "dynFrom", value: v, type: DYN, loc } : L.applyWidthLift(lift, v, tIv, loc);
const body: IrStmt[] = [];
const fieldStmts = new Map<string, IrStmt>();
for (const ff of plan.fields) {
const raw: IrExpr = { kind: "recordGet", obj: sRef, shapeId: plan.fromId, field: ff.name, type: ff.type, loc };
const utag = ff.type.kind === "union" ? L.armTag(ff.type.unionId, UNDEFINED_T) : -1;
@@ -7061,7 +7086,7 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
value: intoSlot(raw, ff.lift),
loc,
};
body.push(
const fieldStmt: IrStmt =
utag >= 0 && ff.type.kind === "union"
? {
kind: "if",
@@ -7070,8 +7095,9 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
else_: null,
loc,
}
: write,
);
: write;
fieldStmts.set(ff.name, fieldStmt);
body.push(fieldStmt);
}
const ksT = arrayOf(STRING);
const fromShape = L.shapes.get(plan.fromId)!;
@@ -7112,7 +7138,8 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
);
}
body.push({ kind: "return", value: tRef, loc });
L.liftedFns.push({
const suffix = body.slice(fieldStmts.size);
const fn: IrFunction = {
name,
params: [
{ localId: "t.0", name: "t", type: toT },
@@ -7132,7 +7159,16 @@ const DV_SETTERS: Record<string, { method: IrBytesIntrinsicMethod; le: boolean }
],
body,
loc,
};
L.shapeOrderHelperFinalizers.push(() => {
const current = L.shapes.get(plan.fromId) ?? fromShape;
const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name);
fn.body = [...currentOrder.flatMap((field) => {
const stmt = fieldStmts.get(field);
return stmt ? [stmt] : [];
}), ...suffix];
});
L.liftedFns.push(fn);
return name;
};
let acc = L.lowerExprExpecting(call.arguments[0]!, targetIr);
@@ -406,7 +406,8 @@ function inspectHelper(L: Lowerer, t: IrType, loc: SrcLoc): string {
{ id: "r.0", name: "r", type: F64, mutable: false },
{ id: "d.0", name: "d", type: F64, mutable: false },
];
let body: IrStmt[];
let body: IrStmt[] = [];
let rebuildShapeOrderBody: (() => void) | null = null;
switch (t.kind) {
case "array": {
@@ -542,21 +543,27 @@ function inspectHelper(L: Lowerer, t: IrType, loc: SrcLoc): string {
body = [ret(str("{}", loc))];
break;
}
// Object.keys' declared order (SEMANTICS.md 36's stance).
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
const byName = new Map(shape.fields.map((f) => [f.name, f.type] as const));
body = [depthGate("[Object]"), ...begin()];
for (const fname of order) {
const ft = byName.get(fname);
if (!ft) continue;
body.push(
entry(
concatAll([str(`${inspectKey(fname)}: `, loc), child(ft, get(fname, ft))], loc),
boolLit(false, loc),
),
);
}
body.push(ret(end(str("", loc), str("{", loc), str("}", loc), false, boolLit(false, loc))));
// Object.keys' declared order (SEMANTICS.md 36's stance). Retained
// reachability can settle this shape after the helper is first
// interned, so rebuild the field walk once metadata is final.
rebuildShapeOrderBody = (): void => {
const current = L.shapes.get(t.shapeId) ?? shape;
const order = current.declaredOrder ?? current.fields.map((f) => f.name);
const byName = new Map(current.fields.map((f) => [f.name, f.type] as const));
body = [depthGate("[Object]"), ...begin()];
for (const fname of order) {
const ft = byName.get(fname);
if (!ft) continue;
body.push(
entry(
concatAll([str(`${inspectKey(fname)}: `, loc), child(ft, get(fname, ft))], loc),
boolLit(false, loc),
),
);
}
body.push(ret(end(str("", loc), str("{", loc), str("}", loc), false, boolLit(false, loc))));
};
rebuildShapeOrderBody();
break;
}
case "map":
@@ -711,6 +718,7 @@ function inspectHelper(L: Lowerer, t: IrType, loc: SrcLoc): string {
throw new Error(`inspect helper over unexpected type ${typeKey(t)}`);
}
let prependCycleGuard: (() => void) | null = null;
if (onCycle) {
// The circular check, FIRST (before the empty-literal and depth
// answers): a value already on the traversal stack renders
@@ -718,24 +726,27 @@ function inspectHelper(L: Lowerer, t: IrType, loc: SrcLoc): string {
// (a circular target beyond the depth budget still says Circular).
locals.push({ id: "cc.0", name: "cc", type: F64, mutable: false });
const cc = (): IrExpr => ref("cc.0", F64);
body.unshift(
{
kind: "varDecl",
localId: "cc.0",
init: { kind: "libCall", fn: "insp.circCheck", args: [v()], type: F64, loc },
loc,
},
{
kind: "if",
cond: { kind: "bin", op: ">", left: cc(), right: num(0, loc), type: BOOL, loc },
then: [ret({ kind: "libCall", fn: "insp.circular", args: [cc()], type: STRING, loc })],
else_: null,
loc,
},
);
prependCycleGuard = (): void => {
body.unshift(
{
kind: "varDecl",
localId: "cc.0",
init: { kind: "libCall", fn: "insp.circCheck", args: [v()], type: F64, loc },
loc,
},
{
kind: "if",
cond: { kind: "bin", op: ">", left: cc(), right: num(0, loc), type: BOOL, loc },
then: [ret({ kind: "libCall", fn: "insp.circular", args: [cc()], type: STRING, loc })],
else_: null,
loc,
},
);
};
prependCycleGuard();
}
L.liftedFns.push({
const fn = {
name,
params: [
{ localId: "v.0", name: "v", type: t },
@@ -746,7 +757,15 @@ function inspectHelper(L: Lowerer, t: IrType, loc: SrcLoc): string {
locals,
body,
loc,
});
};
if (rebuildShapeOrderBody !== null) {
L.shapeOrderHelperFinalizers.push(() => {
rebuildShapeOrderBody!();
prependCycleGuard?.();
fn.body = body;
});
}
L.liftedFns.push(fn);
return name;
}
@@ -653,7 +653,7 @@ export interface FileParts {
}
}
}
const reachable = L.reachable;
const reachable = L.reachableForArtifacts ?? L.reachable;
return {
classes: [...L.classes.values()]
.map((c) => c.def)
@@ -2505,12 +2505,53 @@ export function isParseArgsDynCheckerType(L: Lowerer, type: ts.Type): boolean {
}
}
const emitOrder = restShape.declaredOrder ?? restShape.fields.map((f) => f.name);
if (emitOrder.length !== remaining.length || emitOrder.some((n, i) => n !== remaining[i]!.name)) {
L.unsupported(
"SC1031",
blame,
"rest bindings over class instances whose packed key order cannot match Node's (Object.keys/JSON.stringify would enumerate the copied fields in a different order)",
);
const orderMatches = (order: readonly string[]): boolean =>
order.length === remaining.length && order.every((n, i) => n === remaining[i]!.name);
const deferOrderCheck =
L.instantiationContext !== null &&
L.onEdge !== null &&
!isJsSourceFile(blame.getSourceFile());
if (!deferOrderCheck) {
if (!orderMatches(emitOrder)) {
L.unsupported(
"SC1031",
blame,
"rest bindings over class instances whose packed key order cannot match Node's (Object.keys/JSON.stringify would enumerate the copied fields in a different order)",
);
}
} else {
// Retained reachability can learn about an earlier source-order body
// only while a generic instance lowers. Always defer this metadata-only
// check until that worklist closes: the initial order can settle from
// wrong to right OR from right to wrong. Unlike emitted helper bodies,
// there is no IR to rebuild here — only the settled support decision
// matters.
const context = L.instantiationContext;
const countFailure = !L.suppressStats;
const sf = blame.getSourceFile();
L.shapeOrderMetadataFinalizers.push(() => {
const current = L.shapes.get(restT.shapeId) ?? restShape;
const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name);
if (orderMatches(currentOrder)) return;
L.requiresHistoricalOrderRelower = true;
const previous = L.instantiationContext;
L.instantiationContext = context;
try {
if (countFailure) {
L.stats.statementsFailed++;
L.bumpFileStat(sf.fileName, "failed");
}
L.pushDiag({
code: "SC1031",
message:
"rest bindings over class instances whose packed key order cannot match Node's " +
"(Object.keys/JSON.stringify would enumerate the copied fields in a different order) are not supported yet",
loc: locOf(blame),
});
} finally {
L.instantiationContext = previous;
}
});
}
const fields = remaining.map((f) => {
const fieldType = info.fields.get(f.name) ?? f.type;
@@ -5380,12 +5421,22 @@ function isEsModuleStamp(expr: ts.Expression): boolean {
}
}
const remaining = shape.fields.filter((f) => !consumed.has(f.name));
const restOrder = shape.declaredOrder?.filter((n) => !consumed.has(n));
const restShapeId = L.shapes.intern(
remaining.map((f) => ({ name: f.name, type: f.type })),
false,
undefined,
shape.declaredOrder?.filter((n) => !consumed.has(n)),
restOrder,
);
if (restOrder !== undefined) {
L.shapeOrderMetadataFinalizers.push(
L.shapes.declaredOrderFinalizer(
restShapeId,
restOrder,
() => L.shapes.get(srcType.shapeId)?.declaredOrder?.filter((n) => !consumed.has(n)),
),
);
}
const packed: IrExpr = {
kind: "recordLit",
fields: remaining.map((f) => ({
@@ -83,6 +83,7 @@ import {
isUnitOnlyTsType,
mapType,
ShapeRegistry,
type DeclaredOrderPriorityRef,
typeKey,
type TypeMapperCtx,
UnionRegistry,
@@ -114,6 +115,12 @@ import { fenceCrossBlockNsRef, nsPathPrefix } from "./lower-namespaces.js";
* function can never collide with it (mangling is injective per prefix). */
export const ENTRY_NAME = "%main";
interface GenericDemandOwner {
priority?: readonly [phase: number, order: number];
functionDemands: GenericInstance[];
classDemands: ClassInfo[];
}
/** One step of the copy-reshape width relation (widthLiftPlan): how a
* source-typed value enters a destination slot. Pure data — the plan half;
* applyWidthLift is the build half. */
@@ -370,13 +377,13 @@ export interface LowerOptions {
/** The Lowerer's pass configuration (see lowerToIr). */
export interface LowererMode {
/** Names of bodies the discovery pass reached; null lowers everything. */
/** Names of bodies a prior reachability pass reached; null lowers everything. */
reachable?: ReadonlySet<string> | null;
/** Coverage remainder: lower ONLY bodies outside `reachable`, skip the
* always-reachable init bodies and module building, and report deferred
* collection diagnostics nothing flushed. */
remainder?: boolean;
/** Symbols whose deferred diagnostics the emit pass already flushed —
/** Symbols whose deferred diagnostics reachable emit already flushed —
* the remainder must not report them a second time. */
alreadyFlushed?: ReadonlySet<ts.Symbol>;
/** The build's target platform (LowerOptions.targetPlatform — lowerToIr
@@ -413,24 +420,22 @@ function directExternalTypeSpecifiersByFile(
return out;
}
/** Build lowering runs in two passes over the same ts.Program:
/** Build lowering runs as a reachability worklist over the ts.Program:
*
* 1. DISCOVERY — a worklist computes the set of reachable bodies. Seeds are
* 1. REACHABLE EMIT — a worklist computes the set of reachable bodies.
* Seeds are
* the per-file init bodies (module top-level statements always run, in
* import order); lowering a body yields IR whose call/closure/new/
* virtualCall nodes are the edges that enqueue further bodies. The
* pass's IR, diagnostics, and stats are discarded — it exists only to
* answer "which bodies does the entry reach?".
* 2. EMIT — a fresh Lowerer lowers in the HISTORICAL order (per file:
* function declarations, then class members; then file inits, %main,
* generic instances, lifted lambdas), skipping bodies the discovery
* pass did not mark. Keeping the emit order (and lambda/instance
* numbering) identical to the pre-reachability compiler means a fully
* reachable program emits byte-identical C.
* virtualCall nodes are the edges that enqueue further bodies. The pass
* retains that IR. Once the graph closes, those functions are assembled in
* deterministic declaration order beside the already-lowered init,
* generic-instance, and lifted bodies. Checker-backed IR construction
* therefore happens once instead of once for discovery and again for
* emission.
*
* `coverage: true` adds a third pass — the REMAINDER — that lowers only
* the bodies discovery did NOT mark (plus deferred collection diagnostics
* nothing flushed), reported separately: whole-program analysis without
* `coverage: true` adds a second pass — the REMAINDER — that lowers only
* the bodies reachable emit did NOT mark (plus deferred collection
* diagnostics nothing flushed), reported separately: whole-program analysis without
* letting unreached code fail builds. */
export function lowerToIr(
program: ts.Program,
@@ -438,6 +443,22 @@ export function lowerToIr(
moduleOrder: ts.SourceFile[],
options: LowerOptions = {},
): LowerResult {
const phaseTiming = process.env["SCRIPTC_TIMING"] === "1";
const phaseStarted = performance.now();
let phaseLast = phaseStarted;
const timing = (phase: string, detail: Record<string, unknown> = {}): void => {
if (!phaseTiming) return;
const now = performance.now();
process.stderr.write(
`scriptc lowering ${JSON.stringify({
phase,
phase_ms: Math.round((now - phaseLast) * 10) / 10,
total_ms: Math.round((now - phaseStarted) * 10) / 10,
...detail,
})}\n`,
);
phaseLast = now;
};
const dynamic = options.dynamic ?? false;
const targetPlatform = options.targetPlatform ?? process.platform;
const startupCrash = options.startupCrash ?? null;
@@ -446,9 +467,8 @@ export function lowerToIr(
// pass constructs (nothing calls their %init at startup — the import()
// site's namespace builder does, on the engine microtask, Node's
// evaluation point for them). Inadmissible static cycles inside the
// added subgraph are minted here and handed to the EMIT pass: the
// discovery pass's diagnostics are discarded by design, and after this
// extension of the shared array no later pass re-walks the subgraph.
// added subgraph are minted here and handed to reachable emit after this
// extension of the shared array; no later pass re-walks the subgraph.
const dynamicCycleDiags: ScrDiagnostic[] = [];
if (dynamic) {
appendDynamicImportModules(program, moduleOrder, (cycle, reason) => {
@@ -464,47 +484,66 @@ export function lowerToIr(
directExternalTypeSpecifiersByFile(externalTypes);
const validation = new Lowerer(program, entry, moduleOrder, dynamic, {
targetPlatform,
startupCrash,
ffiImports,
libraryCallbacks,
externalTypes,
externalTypeSpecifiersByFile,
});
const ffiValidation = validateFfiImports(validation);
// Discovery must use the same exact-symbol ownership as emit. Otherwise a
// local function shadowing a configured ambient name is mistaken for FFI
// while computing reachability, even though emit would correctly lower it
// as ordinary TypeScript. FFI-free builds reuse the validation lowerer
// (validation is an immediate no-op there), retaining the historical
// two-pass construction cost.
const discovery = ffiImports.length === 0
timing("ffi-validate");
// Reachability must use the same exact-symbol ownership as FFI validation.
// Otherwise a local function shadowing a configured ambient name is mistaken for FFI
// while computing reachability, even though ordinary lowering would
// correctly handle it as TypeScript. FFI-free builds reuse the validation
// lowerer because validation is an immediate no-op there.
const reachableEmit = ffiImports.length === 0
? validation
: new Lowerer(program, entry, moduleOrder, dynamic, {
targetPlatform,
startupCrash,
ffiImports,
libraryCallbacks,
externalTypes,
externalTypeSpecifiersByFile,
ffiBindingSymbols: ffiValidation.symbolsByName,
});
const reachable = discovery.discover(options.libRoots);
const emit = new Lowerer(program, entry, moduleOrder, dynamic, {
reachable,
targetPlatform,
startupCrash,
ffiImports,
libraryCallbacks,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
});
for (const d of dynamicCycleDiags) emit.pushDiag(d);
for (const d of ffiValidation.diagnostics) emit.pushDiag(d);
const result = emit.run();
for (const d of dynamicCycleDiags) reachableEmit.pushDiag(d);
for (const d of ffiValidation.diagnostics) reachableEmit.pushDiag(d);
const emitted = reachableEmit.emitReachable(options.libRoots);
const { reachable } = emitted;
let result = emitted.result;
let resultLowerer = reachableEmit;
timing("reachable-emit", { reachable: reachable.size });
// A generic class-rest support decision can depend on record metadata
// whose historical owner is discovered only while retained bodies lower.
// If the settled answer is a fence, rerun the ordinary reachable emit so
// the PoisonError occurs in its original statement window: later
// declarators stay unvisited, bindings block, cascades and stats match the
// historical compiler. This is a rare compatibility fallback; programs
// without such a settled fence retain checker-backed IR exactly once.
if (reachableEmit.requiresHistoricalOrderRelower) {
const emit = new Lowerer(program, entry, moduleOrder, dynamic, {
reachable,
targetPlatform,
startupCrash,
ffiImports,
libraryCallbacks,
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
});
for (const d of dynamicCycleDiags) emit.pushDiag(d);
for (const d of ffiValidation.diagnostics) emit.pushDiag(d);
result = emit.run();
resultLowerer = emit;
timing("historical-order-relower");
}
if (options.coverage !== true) return result;
const remainder = new Lowerer(program, entry, moduleOrder, dynamic, {
reachable,
remainder: true,
alreadyFlushed: emit.flushedSymbols,
alreadyFlushed: resultLowerer.flushedSymbols,
targetPlatform,
ffiImports,
libraryCallbacks,
@@ -868,6 +907,152 @@ export class Lowerer {
/** Monomorphization worklist: instances queued by call sites, drained in
* run() (processing an instance body can queue more). */
readonly instantiationQueue: { info: GenericFnInfo; inst: GenericInstance }[] = [];
/** Historical emit rank of the retained declaration/init body currently
* lowering, and the earliest such owner that demanded each generic
* instance. Reachability can encounter a later caller first; the minimum
* rank recovers the old emitter's source-order monomorphization queue. */
private genericDemandOwner: GenericDemandOwner | null = null;
private readonly genericDemandRoots: GenericDemandOwner[] = [];
private readonly genericFunctionDemandOwner = new Map<GenericInstance, GenericDemandOwner>();
private readonly genericClassDemandOwner = new Map<ClassInfo, GenericDemandOwner>();
private readonly genericDemandPriority = new Map<GenericInstance, DeclaredOrderPriorityRef>();
private readonly genericClassDemandPriority = new Map<ClassInfo, DeclaredOrderPriorityRef>();
private withGenericDemandOwner<T>(
owner: GenericDemandOwner,
fn: () => T,
): T {
const previous = this.genericDemandOwner;
this.genericDemandOwner = owner;
try {
return fn();
} finally {
this.genericDemandOwner = previous;
}
}
noteGenericInstanceDemand(inst: GenericInstance): void {
const owner = this.genericDemandOwner;
owner?.functionDemands.push(inst);
const ref = this.genericDemandPriority.get(inst) ?? { rank: [4, Number.MAX_SAFE_INTEGER] };
const currentRank = owner?.priority;
if (currentRank && (
currentRank[0] < ref.rank[0]! ||
(currentRank[0] === ref.rank[0] && currentRank[1] < (ref.rank[1] ?? 0))
)) {
ref.rank = currentRank;
}
this.genericDemandPriority.set(inst, ref);
}
noteGenericClassInstanceDemand(info: ClassInfo): void {
const owner = this.genericDemandOwner;
owner?.classDemands.push(info);
const ref = this.genericClassDemandPriority.get(info) ?? { rank: [4, Number.MAX_SAFE_INTEGER] };
const currentRank = owner?.priority;
if (currentRank && (
currentRank[0] < ref.rank[0]! ||
(currentRank[0] === ref.rank[0] && currentRank[1] < (ref.rank[1] ?? 0))
)) {
ref.rank = currentRank;
}
this.genericClassDemandPriority.set(info, ref);
}
/** The old emitter lowered all reachable declarations in source order,
* then every init, before draining generic instances FIFO. Reorder the
* retained queue from those recorded demands before any generic body
* lowers, so immediate support decisions see the same shape metadata too. */
private restoreGenericInstanceOrder(from = 0): void {
const tail = this.instantiationQueue.slice(from);
const discoveryOrder = new Map(tail.map((entry, index) => [entry.inst, index] as const));
tail.sort((left, right) => {
const a = this.genericDemandPriority.get(left.inst)?.rank;
const b = this.genericDemandPriority.get(right.inst)?.rank;
if (a !== undefined && b !== undefined) {
const phase = a[0]! - b[0]!;
if (phase !== 0) return phase;
const order = a[1]! - b[1]!;
if (order !== 0) return order;
} else if (a !== undefined) {
return -1;
} else if (b !== undefined) {
return 1;
}
return discoveryOrder.get(left.inst)! - discoveryOrder.get(right.inst)!;
});
this.instantiationQueue.splice(from, tail.length, ...tail);
}
private restoreGenericClassInstanceOrder(from = 0): void {
const tail = this.genericClassInstances.slice(from);
const discoveryOrder = new Map(tail.map((info, index) => [info, index] as const));
tail.sort((left, right) => {
const a = this.genericClassDemandPriority.get(left)?.rank;
const b = this.genericClassDemandPriority.get(right)?.rank;
if (a !== undefined && b !== undefined) {
const phase = a[0]! - b[0]!;
if (phase !== 0) return phase;
const order = a[1]! - b[1]!;
if (order !== 0) return order;
} else if (a !== undefined) {
return -1;
} else if (b !== undefined) {
return 1;
}
return discoveryOrder.get(left)! - discoveryOrder.get(right)!;
});
this.genericClassInstances.splice(from, tail.length, ...tail);
}
private settleGenericDemandPriorities(): void {
const functionQueue: GenericInstance[] = [];
const classQueue: ClassInfo[] = [];
const seenFunctions = new Set<GenericInstance>();
const seenClasses = new Set<ClassInfo>();
const enqueue = (owner: GenericDemandOwner): void => {
for (const info of owner.classDemands) {
if (seenClasses.has(info)) continue;
seenClasses.add(info);
classQueue.push(info);
}
for (const inst of owner.functionDemands) {
if (seenFunctions.has(inst)) continue;
seenFunctions.add(inst);
functionQueue.push(inst);
}
};
for (const root of [...this.genericDemandRoots].sort((a, b) => {
const left = a.priority!;
const right = b.priority!;
return left[0] - right[0] || left[1] - right[1];
})) enqueue(root);
let classIndex = 0;
let functionIndex = 0;
let order = 0;
while (classIndex < classQueue.length || functionIndex < functionQueue.length) {
while (classIndex < classQueue.length) {
const info = classQueue[classIndex++]!;
this.genericClassDemandPriority.get(info)!.rank = [4, order++];
const owner = this.genericClassDemandOwner.get(info);
if (owner) enqueue(owner);
}
while (functionIndex < functionQueue.length) {
const inst = functionQueue[functionIndex++]!;
this.genericDemandPriority.get(inst)!.rank = [4, order++];
const owner = this.genericFunctionDemandOwner.get(inst);
if (owner) enqueue(owner);
}
}
for (const info of this.genericClassInstances) {
const ref = this.genericClassDemandPriority.get(info);
if (ref && ref.rank[1] === Number.MAX_SAFE_INTEGER) ref.rank = [4, order++];
}
for (const { inst } of this.instantiationQueue) {
const ref = this.genericDemandPriority.get(inst);
if (ref && ref.rank[1] === Number.MAX_SAFE_INTEGER) ref.rank = [4, order++];
}
}
/** Non-null while an instance body lowers: type-parameter symbol →
* concrete IR type, consulted inside mapType's recursion. */
typeParamBindings: Map<ts.Symbol, IrType> | null = null;
@@ -934,6 +1119,16 @@ export class Lowerer {
/** Synthetic array-HOF loop functions (map/filter/forEach desugar),
* interned per method + element/callback-result type: key → fn name. */
readonly arrHofHelpers = new Map<string, string>();
/** Derived shape metadata that depends on another shape's declaration
* order. These settle before helper bodies rebuild from that metadata. */
readonly shapeOrderMetadataFinalizers: (() => void)[] = [];
/** Settled generic class-rest metadata requires the historical emit
* fallback so its fence can poison the original statement atomically. */
requiresHistoricalOrderRelower = false;
/** Helpers that snapshot shape declaration order into their bodies.
* Reachability lowers inits before the declarations they discover, so
* these rebuild after the worklist restores historical shape metadata. */
readonly shapeOrderHelperFinalizers: (() => void)[] = [];
/** Emit-override specializations (`%C.emit:<event>` — lower-emitter.ts's
* emit-overrides block): interned names, the drive-loop queue, and the
* currently-lowering specialization's context (the super-forward
@@ -1169,7 +1364,7 @@ export class Lowerer {
functionsSkipped: 0,
};
/** Discovery-pass edge sink (null in the emit pass): every resolution of
/** Reachability edge sink: every resolution of
* a reference to a lowerable body reports its name here — recorded even
* when the enclosing statement later poisons. */
onEdge: ((name: string) => void) | null = null;
@@ -1223,9 +1418,12 @@ export class Lowerer {
return this.ctx.scopes;
}
/** Names of bodies the discovery pass reached; null lowers everything
* (the discovery pass itself). */
/** Names of bodies a prior reachability pass reached; null lowers everything. */
readonly reachable: ReadonlySet<string> | null;
/** Reachability computed by this same Lowerer when retained worklist IR
* is assembled directly. The configured reachable set remains null so
* demand-driven instance lowering keeps its existing gates. */
reachableForArtifacts: ReadonlySet<string> | null = null;
/** Coverage remainder mode: the reachability gate inverts (see wantBody)
* and no module is built. */
readonly remainder: boolean;
@@ -1345,9 +1543,7 @@ export class Lowerer {
// --dynamic: modules reachable only through dynamic import() joined
// moduleOrder BEFORE any pass constructed — lowerToIr runs
// appendDynamicImportModules once on the shared array (a per-pass run
// here minted cycle refusals into the DISCOVERY pass, whose
// diagnostics are discarded by design, and the extended order left
// nothing for the emit pass to re-detect).
// here would repeatedly extend the graph and duplicate cycle reports).
this.moduleOrder.forEach((sf, i) => {
this.fileTag.set(sf, sf === entry ? "" : `%m${i}.`);
});
@@ -1383,7 +1579,7 @@ export class Lowerer {
* namespace path (nsPathPrefix), so `namespace A { export class C }`
* and a top-level `class C` never collide. Class EXPRESSIONS name by
* SOURCE POSITION (`%cx<start>.<name>`): deterministic across the
* discovery and emit passes (no counter can drift between them),
* builds (no counter can drift between invocations),
* program-unique through the file qualifier, and collision-free with
* user identifiers ('%'). */
readonly classNamer = (decl: ts.ClassLikeDeclaration): string =>
@@ -2043,8 +2239,8 @@ export class Lowerer {
};
}
/** True when this body should lower: everything with no reachable set
* (discovery), the marked bodies in the emit pass, and exactly the
/** True when this body should lower: everything with no reachable set,
* the marked bodies in an externally-gated pass, and exactly the
* UNMARKED bodies in the coverage remainder. */
wantBody(name: string): boolean {
if (this.reachable === null) return true;
@@ -2104,7 +2300,7 @@ export class Lowerer {
// inline require statements call theirs mid-body, and the guards make
// revisits cache hits — Node's evaluation order over the WHOLE graph
// falls out of the nesting. The coverage remainder skips them — they
// are reachable by definition, already counted by the emit pass.
// are reachable by definition, already counted by reachable emit.
if (!this.remainder) {
for (const fp of parts) {
functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!));
@@ -2190,6 +2386,13 @@ export class Lowerer {
};
}
return this.finishModule(functions);
}
/** Final retention, pruning, and module assembly shared by ordinary emit
* and the retained reachability worklist. */
finishModule(functions: IrFunction[]): LowerResult {
// Globals typed by a class that never REGISTERED (a JS class whose
// collection fenced — Symbol-keyed fields, an unsupported base): the
// declaration statement and every use compiled to runtime fences, but
@@ -2373,7 +2576,7 @@ export class Lowerer {
/** Whether run() counts a signature-blocked declaration in
* stats.functionsSkipped: whole-program passes and the coverage
* remainder do; the reachability emit pass leaves the counting to the
* remainder do; an externally-gated emit pass leaves the counting to the
* remainder (the declaration was never reached). */
countsSkips(): boolean {
return this.reachable === null || this.remainder;
@@ -2381,9 +2584,10 @@ export class Lowerer {
/* ── reachability ─────────────────────────────────────────────────── */
/** The discovery pass: computes the set of body names the program's entry
* reaches. Seeds are the per-file init bodies (top-level statements always
* run); edges fire from RESOLUTION sites while a body lowers (noteEdge /
/** Reachable emit: computes the set of body names the program's entry
* reaches and retains each body IR as it lowers. Seeds are the per-file
* init bodies (top-level statements always run); edges fire from
* RESOLUTION sites while a body lowers (noteEdge /
* noteVirtualEdge) — direct calls, closure creation (a taken closure may
* be called indirectly), `new`, super calls, accessor invocations, and
* virtual dispatch. Recording at resolution time (not off the produced
@@ -2393,19 +2597,20 @@ export class Lowerer {
* demand-driven) and lifted lambdas lower inline with their enclosing
* body; both fire edges through the same hooks and are not units
* themselves. */
discover(extraRoots?: readonly string[]): Set<string> {
emitReachable(extraRoots?: readonly string[]): { reachable: Set<string>; result: LowerResult } {
const parts = this.splitFiles();
this.collectProgram(parts);
// Decorated classes analyze post-collection here too: the %init seeds
// lower the decoration calls, whose edges (decorator bodies, construct
// thunks) the emit pass must see.
// thunks) reachable emit must see.
for (const info of this.classes.values()) analyzeClassDecoration(this, info);
this.prepareModuleInits(parts);
// Every lowerable body, by emitted-function name. The names double as
// the reachable-set keys the emit pass gates on — deterministic across
// Lowerer instances by construction (qualified declaration names).
const units = new Map<string, () => IrFunction | null>();
// retained-function keys and are deterministic by construction
// (qualified declaration names).
const units = new Map<string, { order: number; lower: () => IrFunction | null }>();
let unitOrder = 0;
for (const fp of parts) {
for (const decl of fp.fnDecls) {
// Overload signatures share the implementation's symbol (and so
@@ -2415,7 +2620,7 @@ export class Lowerer {
const declSymbol = declSymbolOf(this, decl);
if (!declSymbol || this.genericFnsBySymbol.has(declSymbol)) continue;
const sig = this.fnSigsBySymbol.get(declSymbol);
if (sig) units.set(sig.name, () => this.lowerFunction(decl));
if (sig) units.set(sig.name, { order: unitOrder++, lower: () => this.lowerFunction(decl) });
}
}
for (const info of this.classes.values()) {
@@ -2428,21 +2633,69 @@ export class Lowerer {
// A FAMILY has no constructor function and no instance members —
// only its statics are units.
if (!info.generic) {
units.set(`%${cName}.constructor`, () => this.lowerClassCtor(info));
units.set(`%${cName}.constructor`, { order: unitOrder++, lower: () => this.lowerClassCtor(info) });
for (const { mName, member } of this.classMethodMembers(info)) {
units.set(`%${cName}.${mName}`, () => this.lowerClassMethodMember(info, member));
units.set(`%${cName}.${mName}`, { order: unitOrder++, lower: () => this.lowerClassMethodMember(info, member) });
}
for (const prop of info.throwingSetters) {
units.set(`%${cName}.set:${prop}`, () => this.throwingSetterFn(info, prop));
units.set(`%${cName}.set:${prop}`, { order: unitOrder++, lower: () => this.throwingSetterFn(info, prop) });
}
}
for (const name of info.staticMethods?.keys() ?? []) {
units.set(`%${cName}.static:${name}`, () => lowerStaticMethod(this, info, name));
units.set(`%${cName}.static:${name}`, { order: unitOrder++, lower: () => lowerStaticMethod(this, info, name) });
}
}
// The old emit pass visited each file's functions and then its classes,
// before every module init. Retained reachability discovers those
// bodies from the inits, but first-seen record metadata still has to
// follow that old order: Object.keys/JSON/inspect observe a shape's
// declaredOrder. Keep output sorting separate — this rank controls only
// that observable metadata.
const metadataPriority = new Map<string, readonly [phase: number, order: number]>();
let declarationMetadataOrder = 0;
const rank = (name: string): void => {
if (units.has(name) && !metadataPriority.has(name)) {
metadataPriority.set(name, [1, declarationMetadataOrder++]);
}
};
for (const fp of parts) {
for (const decl of fp.fnDecls) {
if (!decl.body) continue;
const symbol = declSymbolOf(this, decl);
if (symbol && !this.genericFnsBySymbol.has(symbol)) {
const sig = this.fnSigsBySymbol.get(symbol);
if (sig) rank(sig.name);
}
}
for (const decl of fp.classDecls) {
const info = this.classes.get(this.classNamer(decl));
if (!info) continue;
const cName = info.def.name;
rank(`%${cName}.constructor`);
for (const { mName } of this.classMethodMembers(info)) rank(`%${cName}.${mName}`);
for (const name of info.staticMethods?.keys() ?? []) rank(`%${cName}.static:${name}`);
for (const prop of info.throwingSetters) rank(`%${cName}.set:${prop}`);
}
}
// Defensive fallback for declaration-like units registered outside
// FileParts; they still precede init bodies in the old emit pass.
for (const name of units.keys()) rank(name);
let expressionMetadataOrder = 0;
let instanceMetadataOrder = 0;
const demandOwner = (priority?: readonly [number, number]): GenericDemandOwner => {
const owner: GenericDemandOwner = {
...(priority ? { priority } : {}),
functionDemands: [],
classDemands: [],
};
if (priority) this.genericDemandRoots.push(owner);
return owner;
};
const reachable = new Set<string>();
const queue: string[] = [];
const loweredUnits = new Map<string, IrFunction>();
const initFunctions: IrFunction[] = [];
const instanceFunctions: IrFunction[] = [];
this.onEdge = (name: string): void => {
if (reachable.has(name)) return;
reachable.add(name);
@@ -2453,15 +2706,19 @@ export class Lowerer {
// edge to them can fire (references require the collected class).
this.onExprClassCollected = (info: ClassInfo): void => {
const cName = info.def.name;
units.set(`%${cName}.constructor`, () => this.lowerClassCtor(info));
const register = (name: string, lower: () => IrFunction | null): void => {
units.set(name, { order: unitOrder++, lower });
metadataPriority.set(name, [3, expressionMetadataOrder++]);
};
register(`%${cName}.constructor`, () => this.lowerClassCtor(info));
for (const { mName, member } of this.classMethodMembers(info)) {
units.set(`%${cName}.${mName}`, () => this.lowerClassMethodMember(info, member));
register(`%${cName}.${mName}`, () => this.lowerClassMethodMember(info, member));
}
for (const name of info.staticMethods?.keys() ?? []) {
units.set(`%${cName}.static:${name}`, () => lowerStaticMethod(this, info, name));
register(`%${cName}.static:${name}`, () => lowerStaticMethod(this, info, name));
}
for (const prop of info.throwingSetters) {
units.set(`%${cName}.set:${prop}`, () => this.throwingSetterFn(info, prop));
register(`%${cName}.set:${prop}`, () => this.throwingSetterFn(info, prop));
}
};
// Generic instances queued by the bodies above lower here (an instance
@@ -2477,26 +2734,40 @@ export class Lowerer {
clsInstLowered < this.genericClassInstances.length ||
specLowered < this.emitSpecQueue.length
) {
while (clsInstLowered < this.genericClassInstances.length) {
const info = this.genericClassInstances[clsInstLowered++]!;
const owner = demandOwner();
this.genericClassDemandOwner.set(info, owner);
const ref = this.genericClassDemandPriority.get(info) ?? { rank: [4, instanceMetadataOrder++] };
this.genericClassDemandPriority.set(info, ref);
instanceFunctions.push(...this.withGenericDemandOwner(owner, () =>
this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info))));
}
while (instLowered < this.instantiationQueue.length) {
const { info, inst } = this.instantiationQueue[instLowered++]!;
// Body-level poisons skip the instance here too (the emit pass
// re-records the diagnostic; discovery only needs the edges the
// body fired before poisoning).
const owner = demandOwner();
this.genericFunctionDemandOwner.set(inst, owner);
const ref = this.genericDemandPriority.get(inst) ?? { rank: [4, instanceMetadataOrder++] };
this.genericDemandPriority.set(inst, ref);
// Body-level poisons skip the instance after retaining the
// diagnostic and every edge fired before poisoning.
try {
this.lowerGenericInstance(info, inst);
instanceFunctions.push(this.withGenericDemandOwner(owner, () =>
this.shapes.withDeclaredOrderPriority(ref, () => this.lowerGenericInstance(info, inst))));
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
while (clsInstLowered < this.genericClassInstances.length) {
this.lowerClassMembers(this.genericClassInstances[clsInstLowered++]!);
}
// Emit-override specialization bodies fire edges of their own
// (the super-forward chain, closures, generic calls) — lower them
// for discovery exactly like generic instances.
// exactly like generic instances.
while (specLowered < this.emitSpecQueue.length) {
try {
lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!);
const fn = this.shapes.withDeclaredOrderPriority(
[4, instanceMetadataOrder++],
() => lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!),
);
if (fn) instanceFunctions.push(fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
@@ -2504,9 +2775,18 @@ export class Lowerer {
}
};
parts.forEach((fp) => {
this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!);
drainInstances();
parts.forEach((fp, index) => {
const priority = [2, index] as const;
const owner = demandOwner(priority);
initFunctions.push(
this.withGenericDemandOwner(
owner,
() => this.shapes.withDeclaredOrderPriority(
priority,
() => this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!),
),
),
);
});
// LIBRARY mode's extra reachability roots (LowerOptions.libRoots): the
// profile-mapped exports are called from outside the graph, so they
@@ -2516,20 +2796,70 @@ export class Lowerer {
while (queue.length > 0) {
// A body-level poison outside the per-statement catches (a fenced
// constructor/method parameter default lowered by declareParams):
// discovery only needs the edges the body fired before poisoning —
// the emit pass re-records the diagnostic and skips the member.
// every edge fired before poisoning remains retained; the diagnostic
// stays recorded and the member stays omitted.
try {
units.get(queue.shift()!)!();
const name = queue.shift()!;
const unit = units.get(name)!;
const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++];
const owner = demandOwner(priority);
const fn = this.withGenericDemandOwner(
owner,
() => this.shapes.withDeclaredOrderPriority(priority, unit.lower),
);
if (fn) loweredUnits.set(name, fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
drainInstances();
}
return reachable;
this.restoreGenericInstanceOrder();
this.restoreGenericClassInstanceOrder();
// Generic bodies can reach ordinary declarations, whose bodies can in
// turn queue more instances. Continue to the joint fixpoint; the initial
// queue above is the only portion whose discovery order differed from
// historical emit order.
for (;;) {
drainInstances();
if (queue.length === 0) break;
while (queue.length > 0) {
try {
const name = queue.shift()!;
const unit = units.get(name)!;
const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++];
const owner = demandOwner(priority);
const fn = this.withGenericDemandOwner(
owner,
() => this.shapes.withDeclaredOrderPriority(priority, unit.lower),
);
if (fn) loweredUnits.set(name, fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
this.restoreGenericInstanceOrder(instLowered);
this.restoreGenericClassInstanceOrder(clsInstLowered);
}
const orderedUnits = [...loweredUnits]
.sort(([left], [right]) => units.get(left)!.order - units.get(right)!.order)
.map(([, fn]) => fn);
this.settleGenericDemandPriorities();
this.shapes.settleDeclaredOrderPriorities();
for (const finalize of this.shapeOrderMetadataFinalizers) finalize();
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
const functions = [
...orderedUnits,
...initFunctions,
this.buildMain(),
...instanceFunctions,
...this.liftedFns,
...this.implicitFns,
];
this.reachableForArtifacts = reachable;
return { reachable, result: this.finishModule(functions) };
}
/** Discovery hook (see discover): fires when lowering resolves a
* reference to a lowerable body. Inert in the emit pass. */
/** Reachability hook (see emitReachable): fires when lowering resolves a
* reference to a lowerable body. */
noteEdge(name: string): void {
if (this.onEdge) this.onEdge(name);
}
+128 -2
View File
@@ -61,6 +61,27 @@ export function isParseArgsDynTypeName(name: string): boolean {
return PARSE_ARGS_DYN_TYPES.has(name);
}
export interface DeclaredOrderPriorityRef {
rank: readonly number[];
}
export type DeclaredOrderPriority = readonly number[] | DeclaredOrderPriorityRef;
function priorityRank(priority: DeclaredOrderPriority): readonly number[] {
return "rank" in priority ? priority.rank : priority;
}
function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriority): number {
const a = priorityRank(left);
const b = priorityRank(right);
const length = Math.max(a.length, b.length);
for (let i = 0; i < length; i++) {
const d = (a[i] ?? 0) - (b[i] ?? 0);
if (d !== 0) return d;
}
return 0;
}
/** The frontend's record-shape interner. Records are monomorphic structural
* shapes: fields sorted by name form the canonical identity, and two types
* with the same canonical field list share one shapeId (and later one C
@@ -69,6 +90,22 @@ export function isParseArgsDynTypeName(name: string): boolean {
export class ShapeRegistry {
private readonly byKey = new Map<string, string>();
private readonly byId = new Map<string, IrRecordShape>();
/** Historical emit-order rank of the declaration-order metadata each
* shape kept. Collection runs at rank 0; retained reachability bodies
* install their old emit positions while they lower. This lets a body
* reached after an init replace metadata the worklist encountered first
* when the old emitter would have lowered that body first. */
private readonly declaredOrderPriority = new Map<string, DeclaredOrderPriority>();
private readonly declaredOrderCandidates = new Map<
string,
{ order: string[]; priority: DeclaredOrderPriority }[]
>();
/** Derived shapes whose order is inherited from another shape. The
* source can settle after the derived shape was interned (notably
* Partial<T> inside a retained generic body), so refresh the adopted
* writer after all priority candidates have closed. */
private readonly derivedDeclaredOrderFinalizers: (() => void)[] = [];
private currentDeclaredOrderPriority: DeclaredOrderPriority = [0, 0];
/** All interned shapes in first-seen (`r0`, `r1`, ...) order. */
readonly shapes: IrRecordShape[] = [];
/** ts.Types currently being mapped — a BACK-REFERENCE to one of these is
@@ -104,6 +141,82 @@ export class ShapeRegistry {
);
}
/** Runs one lowering unit under its historical emit-order rank. Shape
* ids remain demand-assigned; only first-seen declaration-order metadata
* uses this rank, because Object.keys/JSON/inspect observe it. */
withDeclaredOrderPriority<T>(priority: DeclaredOrderPriority, fn: () => T): T {
const previous = this.currentDeclaredOrderPriority;
this.currentDeclaredOrderPriority = priority;
try {
return fn();
} finally {
this.currentDeclaredOrderPriority = previous;
}
}
private adoptDeclaredOrder(shape: IrRecordShape, declaredOrder: string[] | undefined): void {
if (declaredOrder === undefined) return;
const candidates = this.declaredOrderCandidates.get(shape.id);
const candidate = { order: declaredOrder, priority: this.currentDeclaredOrderPriority };
if (candidates) candidates.push(candidate);
else this.declaredOrderCandidates.set(shape.id, [candidate]);
const previous = this.declaredOrderPriority.get(shape.id);
if (previous !== undefined && comparePriority(previous, this.currentDeclaredOrderPriority) <= 0) {
return;
}
shape.declaredOrder = declaredOrder;
this.declaredOrderPriority.set(shape.id, this.currentDeclaredOrderPriority);
}
/** Mutable generic-instance ranks settle only after reachability closes.
* Re-choose each shape's first historical writer from the retained
* candidates before derived metadata and helper bodies finalize. */
settleDeclaredOrderPriorities(): void {
for (const [shapeId, candidates] of this.declaredOrderCandidates) {
let best = candidates[0];
if (!best) continue;
for (const candidate of candidates.slice(1)) {
if (comparePriority(candidate.priority, best.priority) < 0) best = candidate;
}
const shape = this.byId.get(shapeId);
if (!shape) continue;
shape.declaredOrder = best.order;
this.declaredOrderPriority.set(shapeId, best.priority);
}
for (const finalize of this.derivedDeclaredOrderFinalizers) finalize();
}
/** Tracks a derived shape that inherits its key order unchanged from a
* source shape. Identity-gating preserves first-writer-wins when an
* equivalent derived shape was already adopted from another source. */
inheritDeclaredOrder(shapeId: string, originalOrder: string[], sourceShapeId: string): void {
this.derivedDeclaredOrderFinalizers.push(
this.declaredOrderFinalizer(
shapeId,
originalOrder,
() => this.get(sourceShapeId)?.declaredOrder,
),
);
}
/** Captures the current historical rank for a derived shape whose order
* must be recomputed after retained reachability settles its source. */
declaredOrderFinalizer(
shapeId: string,
originalOrder: string[],
order: () => string[] | undefined,
): () => void {
return () => {
const shape = this.byId.get(shapeId);
// Only the writer whose array the shape actually adopted may revise
// it. Another structurally-equal type at the same historical rank
// still obeys first-writer-wins.
if (shape?.declaredOrder !== originalOrder) return;
const next = order();
if (next !== undefined) shape.declaredOrder = next;
};
}
/** The shape id a back-reference to an in-progress type resolves to:
* reuses the type's persistent recursive id or mints a PLACEHOLDER
* entry (empty fields) the outer frame finalizes. */
@@ -145,7 +258,7 @@ export class ShapeRegistry {
const shape = this.byId.get(id)!;
shape.fields = fields;
if (indexValue) shape.indexValue = indexValue;
if (declaredOrder) shape.declaredOrder = declaredOrder;
this.adoptDeclaredOrder(shape, declaredOrder);
this.pendingRec.delete(id);
const key = this.keyOf(fields, false, indexValue);
if (!this.byKey.has(key)) this.byKey.set(key, id);
@@ -179,6 +292,14 @@ export class ShapeRegistry {
this.byKey.set(key, id);
this.byId.set(id, shape);
this.shapes.push(shape);
if (declaredOrder !== undefined) {
this.declaredOrderCandidates.set(id, [
{ order: declaredOrder, priority: this.currentDeclaredOrderPriority },
]);
this.declaredOrderPriority.set(id, this.currentDeclaredOrderPriority);
}
} else {
this.adoptDeclaredOrder(this.byId.get(id)!, declaredOrder);
}
return id;
}
@@ -2864,7 +2985,12 @@ function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType |
}
// Fields inherit the source shape's canonical (name-sorted) order — and
// its declaration order (mapped types preserve property order in TS).
return { kind: "record", shapeId: shapes.intern(fields, false, undefined, shape.declaredOrder) };
const originalOrder = shape.declaredOrder;
const shapeId = shapes.intern(fields, false, undefined, originalOrder);
if (originalOrder !== undefined) {
shapes.inheritDeclaredOrder(shapeId, originalOrder, bound.shapeId);
}
return { kind: "record", shapeId };
}
/** The UNIT-ONLY slot type: the interned `null | undefined` union, the one
@@ -5646,6 +5646,12 @@
],
"diags": []
},
"<repo>/tests/corpus/2691-retained-lowering-record-order.ts": {
"order": [
"<repo>/tests/corpus/2691-retained-lowering-record-order.ts"
],
"diags": []
},
"<repo>/tests/corpus/2700-wasi-core.ts": {
"order": [
"<repo>/tests/corpus/2700-wasi-core.ts"
@@ -7702,6 +7708,12 @@
],
"diags": []
},
"<repo>/tests/diagnostics/retained-generic-rest-order.ts": {
"order": [
"<repo>/tests/diagnostics/retained-generic-rest-order.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/runtime-optional-capture-chain.ts": {
"order": [
"<repo>/tests/diagnostics/runtime-optional-capture-chain.ts"
@@ -0,0 +1,162 @@
import { inspect } from "node:util";
// Retained reachability lowers module inits before the function bodies they
// discover. Record metadata and every helper that snapshots it must still use
// historical emit order: these functions' {a,b} shapes precede the inits'
// structurally-equal {b,a} shapes.
function printFunctionRecord(): void {
const value = { a: 1, b: 2 };
console.log(Object.keys(value).join(","));
console.log(Object.values(value).join(","));
console.log(Object.entries(value).map(([k, v]) => `${k}:${v}`).join(","));
console.log(JSON.stringify(value));
console.log(inspect(value));
}
function printIndexRecord(): void {
const value: { a: number; b: number; [key: string]: number } = { a: 1, b: 2 };
console.log(Object.keys(value).join(","));
console.log(Object.values(value).join(","));
console.log(Object.entries(value).map(([k, v]) => `${k}:${v}`).join(","));
}
function printCapturedRecord(): void {
const source = { a: 1, b: 2 };
const value: Record<string, number> = source;
console.log(Object.keys(value).join(","));
}
function printAssignedRecord(): void {
const value: Record<string, unknown> = {};
Object.assign(value, { a: 1, b: 2 });
console.log(Object.keys(value).join(","));
}
function capturedCount(value: Record<string, number>): number {
return Object.keys(value).length;
}
function assignedCount(): number {
const value: Record<string, unknown> = {};
Object.assign(value, { b: 2, a: 1 });
return Object.keys(value).length;
}
interface OrderedNode {
a: number;
b: number;
next: OrderedNode | null;
}
function printRecursiveRecord(): void {
const value: OrderedNode = { a: 1, b: 2, next: null };
value.next = value;
console.log(inspect(value));
}
class GenericOrderedPair {
genericFirst = 1;
genericSecond = 2;
}
function genericSourceFirst<T>(_: T): void {
// This rest lowering checks order immediately; a helper finalizer cannot
// repair it after the wrong generic instance has already fenced.
const { ...value } = new GenericOrderedPair();
console.log(Object.keys(value).join(","));
}
function genericSourceSecond<T>(_: T): void {
const value = { genericSecond: 2, genericFirst: 1 };
console.log(Object.keys(value).length);
}
function callGenericSourceFirst(): void {
genericSourceFirst(1);
}
function callGenericSourceSecond(): void {
genericSourceSecond(1);
}
class GenericClassSourceFirst<T> {
constructor(_: T) {
const { ...value } = new GenericOrderedPair();
console.log(Object.keys(value).join(","));
}
}
class GenericClassSourceSecond<T> {
constructor(_: T) {
const value = { genericSecond: 2, genericFirst: 1 };
console.log(Object.keys(value).length);
}
}
function constructGenericClassSourceFirst(): void {
new GenericClassSourceFirst(1);
}
function constructGenericClassSourceSecond(): void {
new GenericClassSourceSecond(1);
}
class NestedGenericOrderedPair {
nestedFirst = 1;
nestedSecond = 2;
}
function nestedGenericInstanceSourceFirst<T>(_: T): void {
const value = { nestedFirst: 1, nestedSecond: 2 };
console.log(Object.keys(value).length);
}
function nestedGenericSourceFirst(): void {
nestedGenericInstanceSourceFirst(1);
}
function genericReachesEarlierSource<T>(_: T): void {
const value = { nestedSecond: 2, nestedFirst: 1 };
console.log(Object.keys(value).length);
// Reachability learns about this earlier declaration only while this
// instance lowers. Its metadata must still settle before the rest-order
// support decision becomes final.
nestedGenericSourceFirst();
const { ...rest } = new NestedGenericOrderedPair();
console.log(Object.keys(rest).join(","));
}
function printPartialRecord<T extends { a: number; b: number }>(value: T): void {
// Partial<T> derives a fresh shape from T. Its declaration order must
// follow T when retained lowering later settles T to the earlier
// printFunctionRecord body's {a,b} order.
const partial = value as Partial<T>;
console.log(Object.keys(partial).join(","));
}
console.log(Object.keys({ b: 2, a: 1 }).length);
console.log(Object.values({ b: 2, a: 1 }).length);
console.log(Object.entries({ b: 2, a: 1 }).length);
console.log(inspect({ b: 2, a: 1 }).length);
console.log(Object.keys({ b: 2, a: 1 } as { b: number; a: number; [key: string]: number }).length);
console.log(Object.values({ b: 2, a: 1 } as { b: number; a: number; [key: string]: number }).length);
console.log(Object.entries({ b: 2, a: 1 } as { b: number; a: number; [key: string]: number }).length);
console.log(capturedCount({ b: 2, a: 1 }));
console.log(assignedCount());
console.log(inspect({ b: 2, a: 1, next: null } as { b: number; a: number; next: OrderedNode | null }).length);
printPartialRecord({ a: 1, b: 2 });
// Runtime reachability encounters the second caller first. The historical
// emitter visited these caller bodies in source order before it drained the
// generic-instance queue, so the first instance owns the shared shape's key
// order.
callGenericSourceSecond();
callGenericSourceFirst();
constructGenericClassSourceSecond();
constructGenericClassSourceFirst();
genericReachesEarlierSource(1);
printFunctionRecord();
printIndexRecord();
printCapturedRecord();
printAssignedRecord();
printRecursiveRecord();
@@ -0,0 +1,26 @@
class Pair {
a = 1;
b = 2;
}
function earlierGenericSource<T>(_: T): void {
const value = { b: 2, a: 1 };
console.log(Object.keys(value).length);
}
function earlierOrdinary(): void {
earlierGenericSource(1);
}
function laterGeneric<T>(_: T): void {
const sameShape = { a: 1, b: 2 };
console.log(Object.keys(sameShape).length);
earlierOrdinary();
// The deferred rest-order rejection must poison this whole statement:
// the later WeakMap declarator is never visited, and the next statement
// sees rest as a blocked binding just like the historical emit pass.
const { ...rest } = new Pair(), blocked = new WeakMap<object, number>();
console.log(Object.keys(rest).join(","), blocked);
}
laterGeneric(1);
@@ -0,0 +1,15 @@
retained-generic-rest-order.ts:22:11 - error SC1031: rest bindings over class instances whose packed key order cannot match Node's (Object.keys/JSON.stringify would enumerate the copied fields in a different order) are not supported yet (instantiating 'laterGeneric' with <number>)
21 | // sees rest as a blocked binding just like the historical emit pass.
22 | const { ...rest } = new Pair(), blocked = new WeakMap<object, number>();
| ^~~~~~~
23 | console.log(Object.keys(rest).join(","), blocked);
retained-generic-rest-order.ts:23:27 - error SC2004: uses of 'rest' inherit the blocker on its declaration (instantiating 'laterGeneric' with <number>)
22 | const { ...rest } = new Pair(), blocked = new WeakMap<object, number>();
23 | console.log(Object.keys(rest).join(","), blocked);
| ^~~~
24 | }
hint: the declaration of 'rest' did not compile — fix the diagnostic reported there and these sites clear with it
+6
View File
@@ -58,6 +58,12 @@ test("fully static JavaScript program reports 100%", () => {
expect(out).toContain("fully static");
});
test("settled generic rest-order fences count each source statement once", () => {
const { coverage } = analyze(join(repoRoot, "tests/diagnostics/retained-generic-rest-order.ts"));
expect(coverage.diagnostics.map((d) => d.code)).toEqual(["SC1031", "SC2004"]);
expect(coverage.stats.statementsFailed).toBe(2);
});
test("JS inference gaps land where 'any' lands: SC2011 static, island dynamic", async () => {
// The js-gap fixture's tsconfig turns noImplicitAny off, so the untyped
// parameter types `any` — the static analysis reports the site as