Check nullable destructuring sources through their unit arm

The TypeError check for a null or undefined source now reads the unit arm the value holds as a dyn null or undefined, instead of converting the whole union to dyn. A union whose data arm has no dyn form, such as a record holding a generator, compiles again instead of failing with SC9001.
This commit is contained in:
Chris Tate
2026-10-29 10:24:12 -05:00
parent 442c06fde3
commit 7aaf98b53c
2 changed files with 64 additions and 2 deletions
@@ -2581,14 +2581,46 @@ export function lowerDestructuringDecl(
(arm.kind === "array" || arm.kind === "record")
) {
const tag = def.arms.indexOf(arm);
const unionId = init.type.unionId;
const utmp = lowerer.declareHiddenLocal("%destrU", init.type);
out.push({ kind: "varDecl", localId: utmp.id, init, loc });
const uref: IrExpr = { kind: "varRef", localId: utmp.id, type: init.type, loc };
// The failure branch holds one of the unit arms: the check reads it
// as the dyn null or undefined it is, so the data arm needs no dyn
// form (a record holding a generator has none).
const unitValue = (): IrExpr => {
const unitOf = (t: IrType): IrExpr =>
lowerer.coerceToExpected(
{ kind: "unitLit", unit: t.kind === "nullT" ? "null" : "undefined", type: t, loc },
DYN,
);
const units = def.arms.filter(isUnitType);
const last = units[units.length - 1]!;
return units.slice(0, -1).reduceRight<IrExpr>(
(rest, t) => ({
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId,
tag: def.arms.indexOf(t),
negated: false,
value: uref,
type: BOOL,
loc,
},
then: unitOf(t),
else_: rest,
type: DYN,
loc,
}),
unitOf(last),
);
};
const spelling = destrSpellingOf(src);
const check: IrExpr | null = ts.isArrayBindingPattern(decl.name)
? {
kind: "dynIterN",
value: lowerer.coerceToExpected(uref, DYN),
value: unitValue(),
count: decl.name.elements.nodes.length,
...(dynSpell ? { notIterableMessage: dynSpell } : {}),
type: DYN,
@@ -2597,7 +2629,7 @@ export function lowerDestructuringDecl(
: spelling !== null
? {
kind: "dynDestrCheck",
value: lowerer.coerceToExpected(uref, DYN),
value: unitValue(),
spelling,
...firstPatternProp(lowerer, decl.name as ts.ObjectBindingPattern),
type: DYN,
@@ -56,6 +56,36 @@ show("array-function-call", () => {
console.log("never", first);
});
// A source that may be null or undefined, and data arms holding a
// generator, which has no untyped form.
function* count() {
yield 1;
yield 2;
}
function pick(kind) {
if (kind === 0) return null;
if (kind === 1) return undefined;
return { counter: count(), label: "picked" };
}
function counters(present) {
return present ? [count(), count()] : null;
}
show("object-null-arm", () => {
const { counter } = pick(0);
console.log("never", counter);
});
show("object-undefined-arm", () => {
const { label } = pick(1);
console.log("never", label);
});
show("array-generator-elements", () => {
const [first] = counters(false);
console.log("never", first);
});
const { counter, label } = pick(2);
const [left, right] = counters(true);
console.log(label, [...counter].join(","), [...left, ...right].join(","));
// Sources that hold a value bind as before.
const { index } = "abc".match(/b/);
const [whole] = "abc".match(/c/);