mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Preserve record order in retained lowering
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user