mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user