Preserve generic lowering record order

This commit is contained in:
Chris Tate
2026-08-20 11:04:31 -05:00
parent 1f0551d76b
commit dee240efc7
6 changed files with 394 additions and 37 deletions
@@ -1121,6 +1121,7 @@ export function genericFnOf(L: Lowerer, ident: ts.Identifier): GenericFnInfo | n
info.instances.set(key, inst);
L.instantiationQueue.push({ info, inst });
}
L.noteGenericInstanceDemand(inst);
return inst;
}
@@ -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 };
}
@@ -2505,12 +2505,45 @@ 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);
if (!orderMatches(emitOrder)) {
if (L.instantiationContext === null || L.onEdge === null || isJsSourceFile(blame.getSourceFile())) {
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)",
);
}
// Retained reachability can learn about an earlier source-order body
// only while a generic instance lowers. Defer this metadata-only fence
// until that worklist closes; 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;
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;
@@ -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. */
@@ -873,6 +880,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;
@@ -2499,7 +2652,15 @@ export class Lowerer {
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>();
@@ -2543,29 +2704,30 @@ 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++]!;
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 {
instanceFunctions.push(
this.shapes.withDeclaredOrderPriority(
[4, instanceMetadataOrder++],
() => 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) {
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.
@@ -2584,13 +2746,17 @@ export class Lowerer {
};
parts.forEach((fp, index) => {
const priority = [2, index] as const;
const owner = demandOwner(priority);
initFunctions.push(
this.shapes.withDeclaredOrderPriority(
[2, index],
() => this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!),
this.withGenericDemandOwner(
owner,
() => this.shapes.withDeclaredOrderPriority(
priority,
() => this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!),
),
),
);
drainInstances();
});
// LIBRARY mode's extra reachability roots (LowerOptions.libRoots): the
// profile-mapped exports are called from outside the graph, so they
@@ -2605,19 +2771,49 @@ export class Lowerer {
try {
const name = queue.shift()!;
const unit = units.get(name)!;
const fn = this.shapes.withDeclaredOrderPriority(
metadataPriority.get(name) ?? [3, expressionMetadataOrder++],
unit.lower,
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();
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 = [
+53 -9
View File
@@ -61,6 +61,27 @@ export function isParseArgsDynTypeName(name: string): boolean {
return PARSE_ARGS_DYN_TYPES.has(name);
}
export interface DeclaredOrderPriorityRef {
rank: readonly number[];
}
export type DeclaredOrderPriority = readonly number[] | DeclaredOrderPriorityRef;
function priorityRank(priority: DeclaredOrderPriority): readonly number[] {
return "rank" in priority ? priority.rank : priority;
}
function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriority): number {
const a = priorityRank(left);
const b = priorityRank(right);
const length = Math.max(a.length, b.length);
for (let i = 0; i < length; i++) {
const d = (a[i] ?? 0) - (b[i] ?? 0);
if (d !== 0) return d;
}
return 0;
}
/** The frontend's record-shape interner. Records are monomorphic structural
* shapes: fields sorted by name form the canonical identity, and two types
* with the same canonical field list share one shapeId (and later one C
@@ -74,8 +95,12 @@ export class ShapeRegistry {
* 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];
private readonly declaredOrderPriority = new Map<string, DeclaredOrderPriority>();
private readonly declaredOrderCandidates = new Map<
string,
{ order: string[]; priority: DeclaredOrderPriority }[]
>();
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
@@ -114,7 +139,7 @@ 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 {
withDeclaredOrderPriority<T>(priority: DeclaredOrderPriority, fn: () => T): T {
const previous = this.currentDeclaredOrderPriority;
this.currentDeclaredOrderPriority = priority;
try {
@@ -126,19 +151,35 @@ export class ShapeRegistry {
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 &&
(previous[0] < this.currentDeclaredOrderPriority[0] ||
(previous[0] === this.currentDeclaredOrderPriority[0] &&
previous[1] <= this.currentDeclaredOrderPriority[1]))
) {
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);
}
}
/** Captures the current historical rank for a derived shape whose order
* must be recomputed after retained reachability settles its source. */
declaredOrderFinalizer(
@@ -233,6 +274,9 @@ export class ShapeRegistry {
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 {
@@ -54,6 +54,78 @@ function printRecursiveRecord(): void {
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(","));
}
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);
@@ -64,6 +136,15 @@ console.log(Object.entries({ b: 2, a: 1 } as { b: number; a: number; [key: strin
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);
// 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();