mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Keep arrays stored through a typed global view native
A fresh array literal written to a checked-dynamic slot is built as a
dynamic array. On a member of a typed view of the global object
(`(globalThis as typeof globalThis & { cache?: number[] }).cache ??= []`)
the program reads it back through that declaration, and each typed read
extracted a new native copy. `cache().push(x)` was lost, and two reads
were never the same array. Node keeps one array.
When the literal is the right side of a write (`=`, `??=`, `||=`) to a
member of the global object, and the member is declared as a native
array, build the native array and store it boxed. Every typed read then
extracts that same array, and untyped reads see it through its live
capsule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5159d2e966
commit
37ea2c9ce7
@@ -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)),
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7366-typed-global-view-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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<string, unknown>)["__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));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2900, "san": 3200 }
|
||||
Reference in New Issue
Block a user