diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 3080a49b3..5b35be897 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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 }; } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 31564a0a2..e99a53311 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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(); + /** 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:` — 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(); + 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(); 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, diff --git a/packages/compiler/src/frontend/types.ts b/packages/compiler/src/frontend/types.ts index c78f8d2eb..bd2f00108 100644 --- a/packages/compiler/src/frontend/types.ts +++ b/packages/compiler/src/frontend/types.ts @@ -69,6 +69,13 @@ export function isParseArgsDynTypeName(name: string): boolean { export class ShapeRegistry { private readonly byKey = new Map(); private readonly byId = new Map(); + /** 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(); + 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(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; } diff --git a/tests/corpus/2691-retained-lowering-record-order.ts b/tests/corpus/2691-retained-lowering-record-order.ts new file mode 100644 index 000000000..e6ad44928 --- /dev/null +++ b/tests/corpus/2691-retained-lowering-record-order.ts @@ -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);