Count only stores and flows that can reach a typed array's view

This commit is contained in:
Chris Tate
2026-10-22 18:28:44 -05:00
parent c271aa85aa
commit 7cbb6d3b37
2 changed files with 51 additions and 8 deletions
@@ -6312,6 +6312,27 @@ export class Lowerer {
// Element ABIs of the native arrays a value converted to a dynamic type
// can reach: the conversion keeps each such array as a live view, so
// stores through the dynamic value write the native storage.
// A standard library function only reads an array argument it takes as
// `any` (console.log, JSON.stringify, String), except the reflective
// writes: Object.assign and defineProperty, Reflect.set and the like,
// and Array.prototype methods through call or apply.
const reflectiveWriters = new Set([
"assign",
"defineProperty",
"defineProperties",
"setPrototypeOf",
"set",
"apply",
"call",
]);
const standardLibraryReader = (node: ts.CallExpression | ts.NewExpression): boolean =>
panicSafe(() => {
const callee = peel(node.expression);
if (ts.isPropertyAccessExpression(callee) && reflectiveWriters.has(callee.name.text))
return false;
const declaration = this.checker.getResolvedSignature(node)?.declaration;
return declaration !== undefined && this.isStdlibFile(ts.getSourceFileOfNode(declaration));
}, false);
const collectNativeViews = (): Set<string> => {
const keys = new Set<string>();
const seen = new Set<string>();
@@ -6376,8 +6397,9 @@ export class Lowerer {
else if (ts.isReturnStatement(node) && node.expression)
flow(node.expression, contextualOrNull(node.expression));
else if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
for (const arg of node.arguments?.nodes ?? [])
flow(ts.isSpreadElement(arg) ? arg.expression : arg, contextualOrNull(arg));
if (!standardLibraryReader(node))
for (const arg of node.arguments?.nodes ?? [])
flow(ts.isSpreadElement(arg) ? arg.expression : arg, contextualOrNull(arg));
} else if (ts.isPropertyAssignment(node))
flow(node.initializer, contextualOrNull(node.initializer));
else if (ts.isShorthandPropertyAssignment(node)) {
@@ -6398,14 +6420,17 @@ export class Lowerer {
});
return keys;
};
// A receiver typed `any`, `unknown` or an array of either can be a live
// view of a native array. A receiver of another type (`Worker[]`,
// `Record<string, unknown>`) is one only after a lying assertion, even
// when its values are dynamic.
const dynamicType = (type: ts.Type): boolean => {
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true;
if (this.checker.isArrayType(type) && !this.checker.isTupleType(type)) {
const element = this.checker.getTypeArguments(type as ts.TypeReference)[0];
if (element && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0)
return true;
}
return panicSafe(() => this.mapTypeOf(type)?.kind === "dyn", false);
if (!this.checker.isArrayType(type) || this.checker.isTupleType(type)) return false;
const element = this.checker.getTypeArguments(type as ts.TypeReference)[0];
return (
element !== undefined && (element.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0
);
};
const elementStates = new ArrayElementStates({
isDynamicArray: (node) => {
@@ -125,6 +125,24 @@ const view: unknown[] = items;
console.log(view.length, items.findIndex((item) => weigh(item) > 1));
`);
expect(signature(read, "weigh")).toBe("weigh(Item): f64");
// Standard library functions that take arrays as `any` only read them.
const readers = await lower(`${ITEM}
const items: Item[] = [new Item(1)];
console.log(items.length, JSON.stringify(items));
const values: unknown[] = [1];
values[3] = 2;
console.log(items.findIndex((item) => weigh(item) > 1));
`);
expect(signature(readers, "weigh")).toBe("weigh(Item): f64");
// A store through a Record type is not a store through a view.
const record = await lower(`${ITEM}
const items: Item[] = [new Item(1)];
const view: unknown = items;
const bag: Record<string, unknown> = {};
bag["3"] = view;
console.log(items.findIndex((item) => weigh(item) > 1));
`);
expect(signature(record, "weigh")).toBe("weigh(Item): f64");
});
test("holes that only their owning slot can observe stay out of callbacks", async () => {