From f80eb5fc39ce2232abd2fd793241786a20951b84 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 13:39:12 -0500 Subject: [PATCH] Give every arm of record unions that share arms a collector header A shape union's arms all carry a collector header when one of them does. The emitter applied that rule in one pass over the unions in module order, so a header that a later union gave a shared arm did not reach the other arms of an earlier union. With First = A | B and Second = B | C, where only C is cyclic, B got a header through Second, First's trace helper then called A's trace function, and A had none: the module failed LLVM verification with "use of undefined value '@sc_rtrace_r0'". The rule now repeats until no union adds a header. Corpus 5919-record-unions-shared-arms chains Third = D | A, First = A | B and Second = B | C, so the requirement passes from Second through First to Third. --- packages/compiler/src/backend/llvm/emitter.ts | 26 +++++--- .../5919-record-unions-shared-arms.ts.json | 6 ++ .../corpus/5919-record-unions-shared-arms.ts | 60 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5919-record-unions-shared-arms.ts.json create mode 100644 tests/corpus/5919-record-unions-shared-arms.ts diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 775e771ca..26c3b840e 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1395,13 +1395,25 @@ export class LlEmitter { this.tracedUnions = traced.unions; // A shape union's value is whichever record it holds, and the collector // visits it as that record: when one arm carries a collector header, - // every arm must (nullable-unions.ts). - for (const union of this.nullableUnions.allShapeUnions()) - if ( - this.tracedUnions.has(union.unionId) || - union.arms.some((arm) => this.tracedShapes.has(`record:${arm.shapeId}`)) - ) - for (const arm of union.arms) this.tracedShapes.add(`record:${arm.shapeId}`); + // every arm must (nullable-unions.ts). Unions that share an arm pass a + // header on to each other's arms, so repeat until no union adds one. + const shapeUnions = this.nullableUnions.allShapeUnions(); + for (let changed = true; changed;) { + changed = false; + for (const union of shapeUnions) { + if ( + !this.tracedUnions.has(union.unionId) && + !union.arms.some((arm) => this.tracedShapes.has(`record:${arm.shapeId}`)) + ) + continue; + for (const arm of union.arms) { + const key = `record:${arm.shapeId}`; + if (this.tracedShapes.has(key)) continue; + this.tracedShapes.add(key); + changed = true; + } + } + } this.shapeHost = { sizeType: this.sizeType, cycleBufOffset: this.cycleBufOffset, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5919-record-unions-shared-arms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5919-record-unions-shared-arms.ts.json new file mode 100644 index 000000000..bfb23e115 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5919-record-unions-shared-arms.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5919-record-unions-shared-arms.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5919-record-unions-shared-arms.ts b/tests/corpus/5919-record-unions-shared-arms.ts new file mode 100644 index 000000000..89f34dd83 --- /dev/null +++ b/tests/corpus/5919-record-unions-shared-arms.ts @@ -0,0 +1,60 @@ +// Record unions that share arms: Third = D | A, First = A | B and +// Second = B | C. Only C is cyclic. Every arm of a union whose values the +// cycle collector can visit carries a collector header, and the shared +// arms pass that requirement from Second back through First to Third. +type A = { kind: "a"; v: number }; +type B = { kind: "b"; w: number }; +type C = { kind: "c"; kids: C[] }; +type D = { kind: "d"; label: string }; +type Third = D | A; +type First = A | B; +type Second = B | C; + +function third(u: Third): string { + return u.kind === "d" ? u.label : `a${u.v}`; +} + +function first(u: First): number { + return u.kind === "a" ? u.v : u.w; +} + +function second(u: Second): number { + return u.kind === "b" ? u.w : u.kids.length + u.kids.reduce((n, k) => n + second(k), 0); +} + +const zs: Third[] = [ + { kind: "d", label: "dee" }, + { kind: "a", v: 7 }, +]; +console.log(zs.map(third).join(",")); + +const xs: First[] = [ + { kind: "a", v: 1 }, + { kind: "b", w: 2 }, +]; +console.log(xs.map(first).join(",")); + +const leaf: C = { kind: "c", kids: [] }; +const ys: Second[] = [ + { kind: "b", w: 3 }, + { kind: "c", kids: [leaf, { kind: "c", kids: [leaf] }] }, +]; +console.log(ys.map(second).join(",")); + +// One record held through two unions. +const sharedA: A = { kind: "a", v: 5 }; +const sharedB: B = { kind: "b", w: 6 }; +const viaThird: Third = sharedA; +const viaFirst: First[] = [sharedA, sharedB]; +const viaSecond: Second = sharedB; +console.log(third(viaThird), viaFirst.map(first).join(","), second(viaSecond)); + +// A cyclic C reachable only through Second, dropped and rebuilt in a loop. +let total = 0; +for (let i = 0; i < 200; i++) { + const node: C = { kind: "c", kids: [] }; + node.kids.push(node); + const held: Second[] = [node, { kind: "b", w: i }]; + total += held.length + node.kids.length; +} +console.log("total", total); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index c393620c3..8ef52f39a 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2136,6 +2136,7 @@ "5913-prototype-type-error-messages.ts": 3102, "5914-dyn-in-class-prototype-members.ts": 2530, "5915-dyn-error-string-conversions.ts": 3023, + "5919-record-unions-shared-arms.ts": 3182, "5920-promise-try-args.ts": 3028, "5921-promise-try-args-js.js": 3179, "5922-matchall-groups-callbacks.ts": 4418, @@ -5154,6 +5155,7 @@ "5913-prototype-type-error-messages.ts": 3454, "5914-dyn-in-class-prototype-members.ts": 3792, "5915-dyn-error-string-conversions.ts": 3851, + "5919-record-unions-shared-arms.ts": 4941, "5920-promise-try-args.ts": 3195, "5921-promise-try-args-js.js": 3082, "5922-matchall-groups-callbacks.ts": 5275,