diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e737eca64..ccb91291c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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 => { const keys = new Set(); const seen = new Set(); @@ -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`) 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) => { diff --git a/packages/compiler/test/runtime-optional-precision.test.ts b/packages/compiler/test/runtime-optional-precision.test.ts index 3a3251b5a..64f0bd86e 100644 --- a/packages/compiler/test/runtime-optional-precision.test.ts +++ b/packages/compiler/test/runtime-optional-precision.test.ts @@ -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 = {}; +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 () => {