Preserve record order in retained lowering

This commit is contained in:
Chris Tate
2026-08-20 09:18:22 -05:00
parent bc973cf653
commit 4264aca03f
4 changed files with 182 additions and 56 deletions
@@ -6921,50 +6921,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 +6930,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 };
}
@@ -939,6 +939,10 @@ 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>();
/** 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
@@ -2455,6 +2459,43 @@ export class Lowerer {
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 reachable = new Set<string>();
const queue: string[] = [];
@@ -2471,15 +2512,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`, { order: unitOrder++, lower: () => 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}`, { order: unitOrder++, lower: () => this.lowerClassMethodMember(info, member) });
register(`%${cName}.${mName}`, () => this.lowerClassMethodMember(info, member));
}
for (const name of info.staticMethods?.keys() ?? []) {
units.set(`%${cName}.static:${name}`, { order: unitOrder++, lower: () => lowerStaticMethod(this, info, name) });
register(`%${cName}.static:${name}`, () => lowerStaticMethod(this, info, name));
}
for (const prop of info.throwingSetters) {
units.set(`%${cName}.set:${prop}`, { order: unitOrder++, lower: () => this.throwingSetterFn(info, prop) });
register(`%${cName}.set:${prop}`, () => this.throwingSetterFn(info, prop));
}
};
// Generic instances queued by the bodies above lower here (an instance
@@ -2500,20 +2545,33 @@ export class Lowerer {
// Body-level poisons skip the instance after retaining the
// diagnostic and every edge fired before poisoning.
try {
instanceFunctions.push(this.lowerGenericInstance(info, inst));
instanceFunctions.push(
this.shapes.withDeclaredOrderPriority(
[4, instanceMetadataOrder++],
() => this.lowerGenericInstance(info, inst),
),
);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
}
while (clsInstLowered < this.genericClassInstances.length) {
instanceFunctions.push(...this.lowerClassMembers(this.genericClassInstances[clsInstLowered++]!));
instanceFunctions.push(
...this.shapes.withDeclaredOrderPriority(
[4, instanceMetadataOrder++],
() => this.lowerClassMembers(this.genericClassInstances[clsInstLowered++]!),
),
);
}
// Emit-override specialization bodies fire edges of their own
// (the super-forward chain, closures, generic calls) — lower them
// exactly like generic instances.
while (specLowered < this.emitSpecQueue.length) {
try {
const fn = 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;
@@ -2522,8 +2580,13 @@ export class Lowerer {
}
};
parts.forEach((fp) => {
initFunctions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!));
parts.forEach((fp, index) => {
initFunctions.push(
this.shapes.withDeclaredOrderPriority(
[2, index],
() => this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!),
),
);
drainInstances();
});
// LIBRARY mode's extra reachability roots (LowerOptions.libRoots): the
@@ -2538,7 +2601,11 @@ export class Lowerer {
// stays recorded and the member stays omitted.
try {
const name = queue.shift()!;
const fn = units.get(name)!.lower();
const unit = units.get(name)!;
const fn = this.shapes.withDeclaredOrderPriority(
metadataPriority.get(name) ?? [3, expressionMetadataOrder++],
unit.lower,
);
if (fn) loweredUnits.set(name, fn);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
@@ -2548,6 +2615,7 @@ export class Lowerer {
const orderedUnits = [...loweredUnits]
.sort(([left], [right]) => units.get(left)!.order - units.get(right)!.order)
.map(([, fn]) => fn);
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
const functions = [
...orderedUnits,
...initFunctions,
+41 -1
View File
@@ -69,6 +69,13 @@ 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, readonly [phase: number, order: number]>();
private currentDeclaredOrderPriority: readonly [phase: number, order: number] = [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 +111,34 @@ 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: readonly [phase: number, order: number], 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 previous = this.declaredOrderPriority.get(shape.id);
if (
previous !== undefined &&
(previous[0] < this.currentDeclaredOrderPriority[0] ||
(previous[0] === this.currentDeclaredOrderPriority[0] &&
previous[1] <= this.currentDeclaredOrderPriority[1]))
) {
return;
}
shape.declaredOrder = declaredOrder;
this.declaredOrderPriority.set(shape.id, this.currentDeclaredOrderPriority);
}
/** 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 +180,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 +214,11 @@ export class ShapeRegistry {
this.byKey.set(key, id);
this.byId.set(id, shape);
this.shapes.push(shape);
if (declaredOrder !== undefined) {
this.declaredOrderPriority.set(id, this.currentDeclaredOrderPriority);
}
} else {
this.adoptDeclaredOrder(this.byId.get(id)!, declaredOrder);
}
return id;
}
@@ -0,0 +1,11 @@
// Retained reachability lowers module inits before the function bodies they
// discover. Record metadata must still use the historical emit order: this
// function's {a,b} shape precedes the init's structurally-equal {b,a} shape.
function printFunctionRecord(): void {
const value = { a: 1, b: 2 };
console.log(Object.keys(value).join(","));
console.log(JSON.stringify(value));
}
printFunctionRecord();
console.log(Object.keys({ b: 2, a: 1 }).length);