Files
scriptc/tests/corpus/tuple-union-to-array.ts
T
Malte UblandClaude Opus 5.5 4c2adcf123 Convert a union of tuples into an array slot
A value typed as a union of tuples or arrays, such as the `as const`
key-table read `SEQUENCES[functionKey]` in pi, was refused with SC2003
when it flowed into a `string[]` or `readonly string[]` slot. A single
tuple already converted through the width family's tuple-to-array
rebuild; a union of tuples had no plan at all.

widthLiftPlan gains the unionArr step for an array destination: every
non-unit arm must lift on its own tag (a tuple rebuilds through
tupleArr, an array copies or lifts through arr), and a unit arm takes
the stranded TypeError, since the slot has no representation for it.
%union.arr dispatches on the runtime tag and applies each arm's lift;
applyWidthLift and one coerceToExpected hook wire it into the top-level
edges that the nested slots already reach through the plan.

The step is planned only after the union-into-one-of-its-arms rule and
only when no arm of the source is already the destination, so every
union that converted before - `T[] | undefined` into `T[]`, optional
array fields, `??=` - keeps byte-identical IR; only sources with no
conversion at all gain one.

Adds the differential corpus program, its cost file, an IR unit test,
and the order-parity baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:44:46 -07:00

108 lines
3.7 KiB
TypeScript

// A UNION of tuples flowing into an array slot — the `as const` key-table
// shape (`LEGACY_KEY_SEQUENCES[functionKey]`), where TypeScript erases the
// arity for free. The width family's copy stance applies to every arm on
// its own tag: a tuple arm rebuilds as a fresh array (%tup.arr), an array
// arm copies or lifts (%arr), and a union arm that is the slot itself keeps
// it. Every read below must byte-match Node.
// The pi shape: an `as const` table indexed by a literal union, its
// readonly-tuple union passed to a `readonly string[]` parameter.
const SEQUENCES = {
up: ["\x1b[A", "\x1bOA"],
down: ["\x1b[B", "\x1bOB"],
home: ["\x1b[H", "\x1bOH", "\x1b[1~", "\x1b[7~"],
f9: ["\x1b[20~"],
} as const;
type SequenceKey = keyof typeof SEQUENCES;
function matches(data: string, sequences: readonly string[]): boolean {
return sequences.includes(data);
}
function readKey(key: SequenceKey, data: string): boolean {
return matches(data, SEQUENCES[key]);
}
console.log(readKey("up", "\x1bOA"), readKey("f9", "\x1b[20~"), readKey("home", "\x1b[9~"));
console.log(SEQUENCES.up.length, SEQUENCES.home.length, SEQUENCES.f9[0]!.length);
// A union of mutable tuples through every flow edge.
type Variant = ["one"] | ["two", "three"] | ["four", "five", "six"];
function pick(flag: number): Variant {
return flag === 0 ? ["one"] : flag === 1 ? ["two", "three"] : ["four", "five", "six"];
}
function joined(xs: string[]): string {
return xs.join("-");
}
interface Holder {
seqs: readonly string[];
label: string;
}
function holder(flag: number): Holder {
return { seqs: pick(flag), label: `v${flag}` };
}
function nested(flag: number): string[][] {
return [pick(flag)];
}
for (let i = 0; i < 3; i++) {
const variant = pick(i);
// The union itself has no array members; it becomes one at the slot.
const asArray: string[] = variant;
console.log(joined(variant));
console.log(asArray.length, asArray[asArray.length - 1]!.toUpperCase());
for (const item of asArray) console.log("item", item);
const held = holder(i);
console.log(held.label, held.seqs.length, held.seqs.join("/"));
console.log(nested(i)[0]!.join("+"));
// A copy is fresh storage: growing the array leaves the tuple value.
// Only the array side is observed, where Node agrees byte-for-byte.
const grown: string[] = variant;
grown.push("extra", "more");
console.log(grown.length, grown.join(","));
}
// A record-copy field keeps the union conversion on its own arm.
interface Narrow {
seqs: Variant;
label: string;
}
const narrow: Narrow = { seqs: pick(2), label: "n" };
const widened: Holder = narrow;
console.log(widened.label, widened.seqs.length, widened.seqs.join());
// A union of arrays into one wider array slot.
const arrays: string[] | number[] = [1, 2, 3];
const widenedArrays: readonly (string | number)[] = arrays;
console.log(widenedArrays.length, widenedArrays.join(","));
// A missing record key holds undefined; into a `string[]` slot the checked
// conversion throws the catchable TypeError Node raises at the first use.
type Pair = [string, string] | [string];
const table: Record<string, Pair> = { one: ["a", "b"] };
function missingKey(): string[] {
const present: string[] = table["one"]!;
console.log("present", present.length, present.join(""));
try {
const absent: string[] = table["absent"]!;
console.log("no throw", absent.length);
} catch (error) {
console.log("threw", error instanceof TypeError, (error as Error).name);
}
return present;
}
console.log(missingKey().join("|"));
// A narrowed single tuple keeps the existing tuple-to-array path.
const single: ["x", "y"] = ["x", "y"];
const fromSingle: string[] = single;
console.log(fromSingle.length, fromSingle.join(""));
export {};