diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index f42663a13..bff419d48 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -128,6 +128,25 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!); if (route) return route; } + // A record whose fields some arm keeps EVERY one of, each lifting + // (`{ id; limit; opts?: A | B }` into `{ id; limit; opts?: A | B | C } | + // { id; opts?: C }`): that arm is the value's own layout, widened; the + // others hold it only by dropping fields. Prefer the unique lossless arm. + if (selected.length > 1 && src.kind === "record") { + const source = lowerer.shapes.get(src.shapeId); + const lossless = selected.filter(({ arm }) => { + const target = arm.kind === "record" ? lowerer.shapes.get(arm.shapeId) : undefined; + return ( + source !== undefined && + target !== undefined && + !source.tuple && + !target.tuple && + target.fields.length === source.fields.length && + source.fields.every((field) => target.fields.some((kept) => kept.name === field.name)) + ); + }); + if (lossless.length === 1) selected = lossless; + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json new file mode 100644 index 000000000..143cda7d0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-record-lossless-arm.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-record-lossless-arm.ts" + ], + "diags": [] +} diff --git a/tests/corpus/union-record-lossless-arm.ts b/tests/corpus/union-record-lossless-arm.ts new file mode 100644 index 000000000..b90d5486e --- /dev/null +++ b/tests/corpus/union-record-lossless-arm.ts @@ -0,0 +1,55 @@ +// A record flowing into a union where several record arms can hold it by a +// width conversion: the arm that keeps every one of its fields is the +// value's own layout, widened, and wins over arms that drop fields. + +interface FastOpts { + fast?: boolean; + strict?: boolean; +} +interface DepthOpts { + depth?: number; +} +interface StrictOpts { + strict?: boolean; +} + +interface Wide { + id: string; + window: number; + kind?: string; + limit: number; + opts?: FastOpts | DepthOpts | StrictOpts; +} + +interface Judge { + id: string; + window: number; + kind: string; + opts?: StrictOpts; +} + +interface Narrow { + id: string; + window: number; + kind?: string; + limit: number; + opts?: FastOpts | DepthOpts; +} + +function show(entry: Wide | Judge): string { + const limit = "limit" in entry ? ` limit=${entry.limit}` : ""; + return `${entry.kind ?? "wide"} ${entry.id} window=${entry.window}${limit} ${JSON.stringify(entry.opts ?? null)}`; +} + +const narrow: Narrow = { id: "n1", window: 4, limit: 2, opts: { depth: 3 } }; +console.log(show(narrow)); + +const entries: (Wide | Judge)[] = [narrow, { id: "j1", window: 1, kind: "judge", opts: { strict: true } }]; +const plain: Narrow = { id: "n2", window: 8, kind: "custom", limit: 5 }; +entries.push(plain); +for (const entry of entries) console.log(show(entry)); + +let latest: Wide | Judge | undefined; +for (const item of [narrow, plain]) latest = item; +console.log(latest === undefined ? "none" : show(latest)); + diff --git a/tests/harness/corpus-costs/union-record-lossless-arm.ts.json b/tests/harness/corpus-costs/union-record-lossless-arm.ts.json new file mode 100644 index 000000000..011a8fbbf --- /dev/null +++ b/tests/harness/corpus-costs/union-record-lossless-arm.ts.json @@ -0,0 +1 @@ +{ "plain": 11809, "san": 13580 }