diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 15c0ef993..9065e691e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -13145,6 +13145,10 @@ export class Lowerer { ) { return lowerElementAccess(this, expr); } + // `a?.[i]`: the optional chain tests the receiver's undefined arm and + // lowers this access again inside its guard. + if (expr.questionDotToken && !this.chainHandled.has(expr)) + return lowerElementAccess(this, expr); const receiver = this.runtimeOptionalIdentifierValue(expr.expression); const receiverValue: IrExpr | null = receiver && (receiver.present.kind === "array" || receiver.present.kind === "record") diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json new file mode 100644 index 000000000..ef78d4391 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8330-optional-element-undefined-receivers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/8330-optional-element-undefined-receivers.ts" + ], + "diags": [] +} diff --git a/tests/corpus/8330-optional-element-undefined-receivers.ts b/tests/corpus/8330-optional-element-undefined-receivers.ts new file mode 100644 index 000000000..b9433e66c --- /dev/null +++ b/tests/corpus/8330-optional-element-undefined-receivers.ts @@ -0,0 +1,75 @@ +// Optional element access on bindings whose type admits undefined: an +// undefined receiver short-circuits to undefined without evaluating the +// key, and a present receiver reads the element as a plain access does. + +function first(kinds: string[] | undefined, i: number): string { + const kind = kinds?.[i]; + return kind === undefined ? "none" : String(kind); +} +console.log(first(undefined, 0)); +console.log(first(["a", "b"], 1)); +console.log(first(["a"], 5)); + +function orDefault(values: number[] | undefined, i: number): number { + return values?.[i] ?? -1; +} +console.log(orDefault(undefined, 0), orDefault([4, 5], 1), orDefault([4], 3)); + +function isMissing(flags: boolean[] | undefined, i: number): boolean { + return flags?.[i] === undefined; +} +console.log(isMissing(undefined, 0), isMissing([true], 0), isMissing([false], 2)); + +function head(pair: [string, number] | undefined): string { + return pair?.[0] ?? "empty"; +} +console.log(head(undefined), head(["x", 1])); + +function lookup(table: Record | undefined, key: string): number | undefined { + return table?.[key]; +} +console.log(lookup(undefined, "a"), lookup({ a: 1 }, "a"), lookup({ a: 1 }, "b")); + +function width(words: string[] | undefined, i: number): number { + return words?.[i]?.length ?? 0; +} +console.log(width(undefined, 0), width(["abc"], 0), width(["abc"], 1)); + +// The key is not evaluated when the receiver is undefined. +let keys = 0; +function nextKey(): number { + keys++; + return 0; +} +function counted(items: string[] | undefined): string | undefined { + return items?.[nextKey()]; +} +console.log(counted(undefined), keys); +console.log(counted(["k"]), keys); + +// Local bindings, including one reassigned between reads. +function maybe(present: boolean): number[] | undefined { + return present ? [7, 8] : undefined; +} +let current = maybe(false); +console.log(current?.[0]); +current = maybe(true); +console.log(current?.[1]); +current = maybe(false); +console.log(current?.[1]); + +function local(present: boolean): string { + const items: string[] | undefined = present ? ["p", "q"] : undefined; + const second = items?.[1]; + return second ?? "absent"; +} +console.log(local(true), local(false)); + +// A sparse array reads its holes as undefined. +const sparse: number[] = [1]; +sparse[3] = 4; +function at(values: number[] | undefined, i: number): string { + const value = values?.[i]; + return value === undefined ? "hole" : String(value); +} +console.log(at(sparse, 1), at(sparse, 3), at(undefined, 3)); diff --git a/tests/harness/corpus-costs/8330-optional-element-undefined-receivers.ts.json b/tests/harness/corpus-costs/8330-optional-element-undefined-receivers.ts.json new file mode 100644 index 000000000..662eb397f --- /dev/null +++ b/tests/harness/corpus-costs/8330-optional-element-undefined-receivers.ts.json @@ -0,0 +1 @@ +{ "plain": 2108 }