Pack spread tuple fields together with other spreads into optional-element rests

A call that spread a fixed tuple and also an array, a Set or another
iterable into a `(T | undefined)[]` rest parameter took the general rest
path, which converts every value to T, so an undefined tuple field threw
("undefined is not representable in the target union"). Such calls now
pack every item in argument order through lowerArraySpreadItems, which
takes values already lowered (the tuple's fields) beside argument nodes
and keeps undefined values as present undefined elements.

A tuple element typed only `undefined`, as in `...["b", undefined]`, has
a `null | undefined` field. Stored into an array that keeps undefined
elements it is now evaluated and stored as undefined, where it was an
internal compiler error; the checker admits it there only as undefined.
This commit is contained in:
Chris Tate
2026-10-25 04:34:24 -05:00
parent eeb847094e
commit 534c08dd2f
3 changed files with 70 additions and 8 deletions
@@ -407,10 +407,18 @@ export function completeArgs(
// A `(T | undefined)[]` rest (stored as T elements) packs undefined
// arguments as present undefined elements.
const restNodes = sources.slice(restAt);
if (restType.optionalElements === true && !restNodes.some(isIr)) {
out.push(
lowerArraySpreadItems(lowerer, restNodes as ts.Expression[], restType.elem, restType, loc),
);
// Plain arguments and spreads, also together with spread tuple fields
// (pre-lowered): each item, in argument order.
if (
restType.optionalElements === true &&
(!restNodes.some(isIr) || restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a)))
) {
const items: (ts.Expression | IrExpr)[] = [];
for (const a of restNodes) {
if (isIr(a)) items.push(a.ir);
else items.push(a);
}
out.push(lowerArraySpreadItems(lowerer, items, restType.elem, restType, loc));
return out;
}
// Tuple-spread values (pre-lowered) and plain arguments pack the same
@@ -4,6 +4,7 @@ import { generatorDrain } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
import { checkedIterableSpread } from "../checked-iterable-spread.js";
import { AstNode } from "../../ts7/ast-node.js";
import { InternalCompilerError } from "../../../errors.js";
import {
dynUndefinedExpr,
@@ -27,7 +28,9 @@ import {
type IrType,
STRING,
type SrcLoc,
UNDEFINED_T,
arrayOf,
isUnitType,
canConvertToDyn,
funcOf,
isRefCounted,
@@ -57,6 +60,34 @@ import {
} from "./callback-arguments.js";
import { classGeneratorIterable } from "../class-iterables.js";
/** A value whose type admits only null and undefined, stored into an array
* that keeps undefined elements: the checker admits it there only when its
* type is `undefined` (a tuple element typed `undefined` maps to such a
* union), so it is evaluated and stored as undefined. */
function undefinedForNullish(
lowerer: Lowerer,
value: IrExpr,
arrType: IrType & { kind: "array" },
): IrExpr {
if (arrType.optionalElements !== true || value.type.kind !== "union") return value;
const arms = lowerer.unions.get(value.type.unionId)?.arms ?? [];
if (arms.length === 0 || !arms.every(isUnitType)) return value;
const loc = value.loc;
const optional: IrType = {
kind: "union",
unionId: lowerer.unions.intern([arrType.elem, UNDEFINED_T]),
};
const missing = lowerer.wrappedUndefined(optional, loc);
if (!missing) return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: value, loc }],
result: missing,
type: optional,
loc,
};
}
/** Build the argument array for a mutating/copying operation while keeping
* the receiver's scalar payload ABI. A missing read is represented as the
* present UNDEFINED state in this temporary, so push/unshift and toSpliced
@@ -92,7 +123,8 @@ export function lowerArrayValueItems(
loc,
},
];
for (const value of values) {
for (const item of values) {
const value = undefinedForNullish(lowerer, item, arrType);
const stored =
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
optionalElementValue(lowerer, value, elem)
@@ -103,16 +135,21 @@ export function lowerArrayValueItems(
return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc };
}
/** The items of an array built from argument nodes (a spread copies its
* elements in) and values already lowered (a spread tuple's fields), in
* order. Each value is stored when its turn comes. */
export function lowerArraySpreadItems(
lowerer: Lowerer,
nodes: readonly ts.Expression[],
nodes: readonly (ts.Expression | IrExpr)[],
elem: IrType,
arrType: IrType & { kind: "array" },
loc: SrcLoc,
): IrExpr {
const out = lowerer.declareHiddenLocal("%arrayItems", arrType);
const outRef = varRef(out.id, arrType, loc);
const plainItems = nodes.filter((node) => !ts.isSpreadElement(node)).length;
const plainItems = nodes.filter(
(node) => !(node instanceof AstNode) || !ts.isSpreadElement(node),
).length;
const length = (): IrExpr => ({
kind: "arrIntrinsic",
method: "length",
@@ -136,7 +173,15 @@ export function lowerArraySpreadItems(
},
];
for (const node of nodes) {
if (ts.isSpreadElement(node)) {
if (!(node instanceof AstNode)) {
const value = undefinedForNullish(lowerer, node, arrType);
const stored =
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
optionalElementValue(lowerer, value, elem)
? value
: lowerer.coerceToExpected(value, elem);
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
} else if (ts.isSpreadElement(node)) {
let source = lowerer.lowerExpr(node.expression);
if (
source.type.kind === "set" &&
@@ -49,6 +49,15 @@ function swap(pair: [string, string | undefined]): string {
return "v";
}
console.log(label("z", ...swapped, swap(swapped), ...swapped));
// Tuple spreads together with array and Set spreads keep undefined
// elements, and every item is read in argument order.
const mixedPair: [boolean, boolean | undefined] = [true, undefined];
const mixedMore: (boolean | undefined)[] = [false, undefined];
console.log(flags(...mixedPair, ...mixedMore), flags(...mixedMore, ...mixedPair), flags(true, ...mixedPair, ...mixedMore, undefined));
const mixedChanging: [boolean, boolean | undefined] = [true, undefined];
console.log(flags(...mixedChanging, ...mixedMore, flip(mixedChanging, true), ...mixedChanging));
const mixedWords: [string, string | undefined] = ["a", undefined];
console.log(label("z", ...["b", undefined, "c"], ...mixedWords), label("y", ...mixedWords, ...new Set(["s", undefined])));
function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string {
let a = start;