diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4c417e86e..554b703e9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10609,6 +10609,38 @@ export class Lowerer { /** Lower for a known destination, then apply ordinary value coercion. * Fresh arrays and records use the destination layout directly. */ + /** `value` is the right side of a write to a member of a typed view of the + * global object (`(globalThis as typeof globalThis & { k?: T }).k = value`, + * also through `??=` and `||=`). */ + private globalViewSlot(value: ts.Expression): boolean { + const write = value.parent; + if ( + !ts.isBinaryExpression(write) || + write.right !== value || + (write.operatorToken.kind !== ts.SyntaxKind.EqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken && + write.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) + ) + return false; + let target = write.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) + return false; + const receiver = this.typeOf(target.expression); + const isGlobal = (type: ts.Type): boolean => { + const symbol = type.getSymbol(); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "globalThis" && + symbol.declarations.length === 0 + ); + }; + return ( + isGlobal(receiver) || + (receiver.isIntersectionType() && ts.constituentTypes(receiver).some(isGlobal)) + ); + } + lowerExprExpecting(node: ts.Expression, expected: IrType | undefined): IrExpr { // These wrappers do not change the value or its inferred type. Keep a // fresh literal visible to destination-directed construction. Assertions @@ -10674,6 +10706,20 @@ export class Lowerer { (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { + // A checked slot that the program declares as a native array (a + // member of a typed view of the global object, `typeof globalThis & + // { cache?: number[] }`) is read back through that declaration. Build + // the native array and store it boxed: every typed read then extracts + // that same array (a dynamic literal would be copied on each read, + // losing writes such as push). + const slotType = this.globalViewSlot(x) ? this.checker.getContextualType(x) : undefined; + const declared = slotType && this.checker.getNonNullableType(slotType); + const native = + declared && !(declared.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) + ? this.mapTypeOf(declared) + : null; + if (native?.kind === "array") + return this.coerceInto(node, this.lowerArrayLiteral(x, native), DYN); return { kind: "dynArrLit", elems: x.elements.nodes.map((element) => this.lowerExprExpecting(element, DYN)), diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..49da80b55 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7366-typed-global-view-arrays.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7366-typed-global-view-arrays.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7366-typed-global-view-arrays.ts b/tests/corpus/7366-typed-global-view-arrays.ts new file mode 100644 index 000000000..3b24620d5 --- /dev/null +++ b/tests/corpus/7366-typed-global-view-arrays.ts @@ -0,0 +1,32 @@ +// Arrays stored on the global object through a typed view are the same +// arrays every typed read returns: writes through one read (push, index +// assignment, sort) are visible through the next, as in Node. +type Store = typeof globalThis & { __exampleCache?: number[]; __exampleNames?: string[] }; + +function cache(): number[] { + const g = globalThis as Store; + g.__exampleCache ??= [1]; + return g.__exampleCache; +} + +cache().push(2); +cache().push(3); +cache()[0] = 10; +console.log(cache().length, cache().join(","), cache() === cache()); + +const view = globalThis as Store; +view.__exampleNames = ["b", "a"]; +const names = view.__exampleNames; +names.push("c"); +names.sort(); +console.log(view.__exampleNames.join(","), names === view.__exampleNames); + +// the same array seen through an untyped read +const raw = (globalThis as unknown as Record)["__exampleCache"]; +console.log(Array.isArray(raw), JSON.stringify(raw), raw === cache()); + +// a literal of another element type stays its own value +const counts = globalThis as typeof globalThis & { __exampleCounts?: number[][] }; +counts.__exampleCounts = [[1], [2, 3]]; +counts.__exampleCounts[1]!.push(4); +console.log(JSON.stringify(counts.__exampleCounts)); diff --git a/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json new file mode 100644 index 000000000..27b641ffe --- /dev/null +++ b/tests/harness/corpus-costs/7366-typed-global-view-arrays.ts.json @@ -0,0 +1 @@ +{ "plain": 2900, "san": 3200 }