diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 29ddf3aace..aa23fb0a2e 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -38,6 +38,28 @@ import { arrayValueStore, primitiveElement } from "../array-values.js"; import { classGeneratorIterable } from "../class-iterables.js"; import { iteratorCanStep } from "../iterator-consumption.js"; +/** The arm an EMPTY `[]` literal builds as: `arms`' first array arm, or + * null when the literal is not empty (an element selects the arm) or no + * arm is an array. + * + * An empty literal carries no element of its own — tsc infers `never[]`, + * whose f64-element mapping is the uninhabited element's representation, + * not one of the destination's arms — and every empty array is the same + * value whichever array family stores it, so several array arms raise no + * ambiguity. This is the ONE statement of that rule: a union slot, a + * contextual union, and the `??` default all ask it. A lone array slot + * states the same thing without asking. */ +export function emptyArrayLiteralArm( + node: ts.Expression, + arms: readonly IrType[], +): (IrType & { kind: "array" }) | null { + let value = node; + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (!ts.isArrayLiteralExpression(value) || value.elements.nodes.length > 0) return null; + const arm = arms.find((candidate) => candidate.kind === "array"); + return (arm as (IrType & { kind: "array" }) | undefined) ?? null; +} + /** JavaScript: whether `expr` is or holds an existing checked-dynamic * object — a binding stored as a dyn value whose checker type is an * object, or an array or object literal with such an element. Building @@ -206,16 +228,12 @@ export function lowerArrayLiteral( mapped = arrayOf(unitOnlyUnion(lowerer.unions)); // An EMPTY literal in a union slot (`const t: string[] | undefined = // []`, the `[]` default argument against `string[] | undefined`) has - // no useful own type — tsc infers `never[]`, whose f64-element - // mapping is a representation for the uninhabited, not the slot's - // arm. Build the literal as the union's FIRST array arm and let the - // slot's coercion wrap it: an empty array is the same value whichever - // array family stores it, so several array arms raise no ambiguity — - // the rule a lone array slot already states. - if (ctxUnion && expr.elements.nodes.length === 0) { + // no useful own type: it builds as the union's first array arm (the + // emptyArrayLiteralArm rule) and the slot's coercion wraps it. + if (ctxUnion) { const def = lowerer.unions.get(ctxUnion.unionId); - const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? []; - if (arrayArms.length >= 1) mapped = arrayArms[0]!; + const arm = emptyArrayLiteralArm(expr, def?.arms ?? []); + if (arm) mapped = arm; } // A NON-EMPTY literal whose own type has no static home (the JS dyn // fallback — a null/dyn mapping, or an inference that degraded to a diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index a6d7ad674d..c204b40f4c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -73,6 +73,7 @@ import { nsWritableTarget } from "../lower-namespaces.js"; import { arrayValueStore, tryLowerBytesReadNumberEquality } from "../array-values.js"; import { cjsExportMemberWriteGlobalOf, cjsInFileExportGlobalOf } from "../cjs-exports-object.js"; import { lowerUnionFieldWrite } from "./union-fields.js"; +import { emptyArrayLiteralArm } from "./array-literals.js"; import { lowerAbstractEquality } from "../abstract-equality.js"; import { lowerInExpression } from "./relational.js"; import { lowerDynTypeofTest, lowerErrorCodeTypeofTest } from "./typeof.js"; @@ -380,19 +381,6 @@ function untypedCollectionFallback(lowerer: Lowerer, node: ts.Expression): boole }); } -/** An empty `[]` literal whose own type is `never[]` (no contextual array - * type supplied one): the `??` default of an optional array read. */ -function emptyNeverArrayLiteral(lowerer: Lowerer, node: ts.Expression): boolean { - let value = node; - while (ts.isParenthesizedExpression(value)) value = value.expression; - if (!ts.isArrayLiteralExpression(value) || value.elements.nodes.length > 0) return false; - const own = lowerer.typeOf(value); - return ( - lowerer.checker.isArrayType(own) && - (lowerer.checker.getTypeArguments(own as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0 - ); -} - export function lowerNullishPair( lowerer: Lowerer, expr: ts.BinaryExpression, @@ -445,21 +433,21 @@ export function lowerNullishPair( return { kind: "nullish", left, right, type: rest[0]!, loc }; } // An EMPTY array-literal default (`''.match(re) ?? []`) has no element of - // its own: tsc infers `never[]`, whose f64-element mapping the whole - // expression keeps as a second array arm (`RegExpMatchArray | number[]`), - // a result no single array slot can receive. It adopts the left's present - // array arm instead — the empty-literal rule the union slot and the - // ternary arm already state — so `?? []` answers the left's array type. - // A JavaScript `[]` is `any[]` and keeps the dynamic path. + // its own, so it builds as the left's array arm (the emptyArrayLiteralArm + // rule the union slot and the contextual union share) and the whole + // expression answers the left's array type instead of the checker's + // `never[]`-flavored second arm. A JavaScript `[]` is `any[]` and keeps + // the dynamic path. if ( rest.length === 1 && rest[0]!.kind === "array" && - !isJsSourceFile(ts.getSourceFileOfNode(expr.right)) && - emptyNeverArrayLiteral(lowerer, expr.right) + !isJsSourceFile(ts.getSourceFileOfNode(expr.right)) ) { - const right = lowerer.lowerExprExpecting(expr.right, rest[0]!); - if (typeEquals(right.type, rest[0]!)) - return { kind: "nullish", left, right, type: rest[0]!, loc }; + const arm = emptyArrayLiteralArm(expr.right, rest); + if (arm) { + const right = lowerer.lowerExprExpecting(expr.right, arm); + if (typeEquals(right.type, arm)) return { kind: "nullish", left, right, type: arm, loc }; + } } let type = lowerer.irTypeOf(expr); // A runtime-optional read can reach the default even when declarations diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 1fde89a550..53849353e9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -781,6 +781,7 @@ import { lowerRecordFieldCall, lowerObjectMethodCall } from "./lower-calls.js"; import { fenceCrossBlockNsRef, nsPathPrefix } from "./lower-namespaces.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { partialAssertionFields } from "./partial-assertions.js"; +import { emptyArrayLiteralArm } from "./expressions/array-literals.js"; import { lowerTernary } from "./lower-exprs.js"; import { maybePromiseReturnStmt, @@ -14044,15 +14045,10 @@ export class Lowerer { ); } } - // An EMPTY literal carries no element to select an arm by: tsc - // infers `never[]`, whose f64-element mapping is the uninhabited - // element's representation, not one of the slot's arms. Every empty - // array is the same value whichever array arm stores it, so the - // literal is built as the union's FIRST array arm — the empty - // literal rule a lone array slot already states, and the same one - // the ternary arm and the `?? []` default apply. - if (!arm && arms.length > 1 && x.elements.nodes.length === 0) - arm = arms.find((candidate) => candidate.kind === "array"); + // An EMPTY literal carries no element to select an arm by: it + // builds as the union's first array arm (the emptyArrayLiteralArm + // rule the contextual union and the `?? []` default share). + if (!arm) arm = emptyArrayLiteralArm(x, arms) ?? undefined; if (arm) { const built = this.lowerArrayLiteral( x,