fix(javascript): stop recording replaced slot values as store receivers (#7128)

Reference-store identity queries resolved the written target as a value,
so once a step consumed the written key the walk continued into the value
read from that slot and recorded it, and everything reachable from it, as
an object the store mutates. Every value stored into one local object at
unknown keys then received an entry for every such store, and each new
store re-queued every earlier one. Ambient-write indexing grew cubically
in the number of such stores.

Receiver queries now drop items whose projection no longer ends at the
written key. Stored-value selection has one owner, which the resolver and
the store fixpoint share. The fixpoint re-queues a dependent only when a
changed entry can reach it with a selection that its receiver query keeps.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-25 05:29:23 -07:00
committed by GitHub
parent 5a22ae6bc8
commit 70b96b65d9
2 changed files with 189 additions and 49 deletions
@@ -1,3 +1,4 @@
import * as t from "@babel/types";
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
@@ -128,3 +129,114 @@ box.trigger;`;
).toEqual(["box.trigger", "box.trigger"]);
expect(effects[0]?.node).not.toBe(effects[1]?.node);
});
const globalFetchWriteSites = (body: string): string[] => {
const source = `var t={},k=[];${body}`;
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected stored-slot fixture");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
return state.ambientWriteEffects
.filter(
({ path }) =>
JSON.stringify(path) === JSON.stringify(["global", "fetch"]),
)
.map(({ node }) =>
node === undefined ? "" : source.slice(node.start ?? 0, node.end ?? 0),
);
};
it.each([
{
name: "an object read from a replaced slot",
body: "var g={};t[k[0]]=g;t[k[1]]=window;g.fetch=replacement;",
sites: [],
},
{
name: "a literal property value whose slot is replaced",
body: "var o={};var h={p:o};h.p=window;o.fetch=replacement;h.p.fetch=other;",
sites: ["h.p.fetch=other"],
},
{
name: "a global read back through an unknown slot",
body: "t[k[0]]=window;t[k[1]].fetch=replacement;",
sites: ["t[k[1]].fetch=replacement"],
},
{
name: "a global read into an alias of an unknown slot",
body: "t[k[0]]=window;var r=t[k[1]];r.fetch=replacement;",
sites: ["r.fetch=replacement"],
},
{
name: "a nested store through an unknown slot",
body: "t[k[0]]={};t[k[1]].x=window;t[k[2]].x.fetch=replacement;",
sites: ["t[k[2]].x.fetch=replacement"],
},
{
name: "a store through an instance prototype",
body: "function F(){}var x=new F();x.__proto__.slot=window;F.prototype.slot.fetch=replacement;",
sites: ["F.prototype.slot.fetch=replacement"],
},
{
name: "a store through an instance constructor",
body: "function F(){}var x=new F();x.constructor.slot=window;F.slot.fetch=replacement;",
sites: ["F.slot.fetch=replacement"],
},
{
name: "a store into a rest copy",
body: "var src={};var {...copy}=src;copy.slot=window;copy.slot.fetch=replacement;",
sites: ["copy.slot.fetch=replacement"],
},
])("resolves stored references for $name", ({ body, sites }) => {
expect(globalFetchWriteSites(body)).toEqual(sites);
});
it("keeps an object read from a replaced slot as a URL base proof", () => {
const worker =
analyzeJavaScriptStaticSource(`import { Worker } from "node:worker_threads";
const t={},k=[];const g={};t[k[0]]=g;t[k[1]]=globalThis;g.URL=replacement;
new Worker(new URL("./worker.js", import.meta.url));`).references.find(
(reference) => reference.kind === "worker",
);
expect(worker?.specifier).toBe("./worker.js");
});
it("indexes unknown-key stores of fresh objects in linear stored-value work", () => {
// Count reads of the stored values' own syntax during ambient indexing only.
const work = (count: number): number => {
const source = `var t={},k=[];${Array.from({ length: count }, (_, i) => `t[k[${String(i)}]]={v:${String(i)}};`).join("")}`;
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("Expected stored-object fixture");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
let counting = false;
let reads = 0;
for (const statement of parsed.program.body) {
if (
!t.isExpressionStatement(statement) ||
!t.isAssignmentExpression(statement.expression)
)
continue;
const value = statement.expression.right;
const type = value.type;
Object.defineProperty(value, "type", {
enumerable: true,
configurable: true,
get: () => {
if (counting) reads += 1;
return type;
},
});
}
counting = true;
completeSemanticSteps(index.indexAmbientWritesSteps());
counting = false;
expect(index.bindings.ambientWriteEffects).toEqual([]);
return reads;
};
// Each stored value is examined a bounded number of times. Re-reading every
// earlier value from every later store grows at least quadratically.
expect(work(64)).toBeLessThan(3 * work(32));
});
@@ -83,6 +83,40 @@ export const retainSemanticAmbientEffect = (
/** Undefined means a cyclic or conflicting projection can select any depth. */
type AmbientProjection = AmbientPath | undefined;
/**
* Select the stored value an object's reference store exposes to a projection,
* or null when the store cannot reach it. A shorter projection only reaches
* stored children when the query covers escaped contents.
*/
const storedValueProjection = (
suffix: AmbientProjection,
source: SemanticIterationSource,
path: AmbientProjection,
escapedContents: boolean,
): AmbientProjection | null => {
if (
suffix !== undefined &&
path !== undefined &&
(!path.every(
(key, index) =>
index >= suffix.length ||
key === null ||
suffix[index] === null ||
key === suffix[index],
) ||
(suffix.length < path.length && !escapedContents))
)
return null;
return suffix === undefined || path === undefined
? undefined
: [
...source.projection.map((key) =>
key === null || typeof key === "object" ? null : String(key),
),
...suffix.slice(path.length),
];
};
const joinAmbientProjection = (
left: AmbientProjection,
right: AmbientProjection,
@@ -129,7 +163,7 @@ export const createAmbientPathResolver = (
capture?: t.Node,
escapedContents = false,
projection: AmbientProjection = [],
localIdentity?: (node: t.Node, path: AmbientProjection) => void,
receiver?: (node: t.Node, path: AmbientProjection) => void,
): AmbientResolution => {
const prior = possibleWrites ? undefined : cached.get(node);
if (prior !== undefined) return prior;
@@ -138,11 +172,20 @@ export const createAmbientPathResolver = (
const unknownReasons = new Set<AmbientIdentityUnknownReason>();
// The caller's write site selects initializers for the written target.
// Reads reached through an initializer occur at their own position.
const pending: {
type Pending = {
node: t.Node;
suffix: AmbientProjection;
readsAtSelf?: true;
}[] = [{ node, suffix: projection }];
};
const pending: Pending[] = [{ node, suffix: projection }];
// A receiver query reports the objects a write to `node` mutates, as
// projections that still end at the written key. Once a step consumes that
// key, the item denotes the value read from the written slot. Replacing
// the slot does not mutate that value or the containers it was read from.
const queue = (item: Pending): void => {
if (receiver !== undefined && item.suffix?.length === 0) return;
pending.push(item);
};
// Join projections at their producing expression, rather than enumerating
// alias histories. Each key can only widen to a wildcard, and conflicting
// depths widen to unknown, so diamonds and cycles have finite state.
@@ -213,7 +256,7 @@ export const createAmbientPathResolver = (
// A widened projection is a possible write, never proof of an intrinsic.
if (suffix === undefined && !possibleWrites) continue;
const continueWith = (node: t.Node, path: AmbientProjection = suffix) =>
pending.push({
queue({
node,
suffix: path,
...(current.readsAtSelf && { readsAtSelf: true }),
@@ -239,50 +282,21 @@ export const createAmbientPathResolver = (
);
};
if (!t.isIdentifier(root) && facts?.freshObject(root)) {
localIdentity?.(root, suffix);
receiver?.(root, suffix);
// The allocation owns itself; a projected child needs its own proof.
if (suffix?.length === 0) {
hasLocalIdentity = true;
resolvedIdentity = true;
}
for (const [source, path] of facts?.stores.get(root) ?? []) {
const selected =
suffix === undefined ||
path === undefined ||
path.every(
(key, index) =>
index >= suffix.length ||
key === null ||
suffix[index] === null ||
key === suffix[index],
);
if (
!selected ||
(suffix !== undefined &&
path !== undefined &&
suffix.length < path.length &&
!escapedContents)
)
continue;
const remaining =
suffix === undefined || path === undefined
? undefined
: suffix.slice(path.length);
pending.push({
node: source.node,
readsAtSelf: true,
suffix:
remaining === undefined
? undefined
: [
...source.projection.map((key) =>
key === null || typeof key === "object"
? null
: String(key),
),
...remaining,
],
});
const stored = storedValueProjection(
suffix,
source,
path,
escapedContents,
);
if (stored === null) continue;
queue({ node: source.node, readsAtSelf: true, suffix: stored });
}
}
if (
@@ -340,7 +354,7 @@ export const createAmbientPathResolver = (
escapedContents ? "escape" : "identity",
);
if (path === null) continue;
pending.push({
queue({
node: initializer.node,
readsAtSelf: true,
suffix:
@@ -667,17 +681,19 @@ export function* indexJavaScriptAmbientWritesSteps(
},
);
const pendingStores = new Set(storedReferences);
const dependents = new WeakMap<t.Node, Set<SemanticIterationSource>>();
// Each dependent keeps the joined projection its latest query reached.
const dependents = new WeakMap<
t.Node,
Map<SemanticIterationSource, AmbientProjection>
>();
while (pendingStores.size > 0) {
const source = pendingStores.values().next().value;
if (source === undefined) break;
pendingStores.delete(source);
if (source.target === undefined) continue;
const identities = new Map<t.Node, AmbientProjection>();
// Record the objects this store mutates, not the values it replaces.
ambientWrites(source.target, source.mutation, false, [], (node, path) => {
const users = dependents.get(node) ?? new Set<SemanticIterationSource>();
users.add(source);
dependents.set(node, users);
identities.set(
node,
identities.has(node)
@@ -685,6 +701,13 @@ export function* indexJavaScriptAmbientWritesSteps(
: path,
);
});
for (const [node, path] of identities) {
const users =
dependents.get(node) ??
new Map<SemanticIterationSource, AmbientProjection>();
users.set(source, path);
dependents.set(node, users);
}
// Publish only completed queries; each source/root projection can only widen.
for (const [node, path] of identities) {
const entries =
@@ -703,8 +726,13 @@ export function* indexJavaScriptAmbientWritesSteps(
continue;
entries.set(source, joined);
stores.set(node, entries);
for (const dependent of dependents.get(node) ?? [])
pendingStores.add(dependent);
// A receiver query drops a value read from its written slot, so only a
// nonempty or widened selection can change a dependent's identities.
for (const [dependent, reached] of dependents.get(node) ?? []) {
const stored = storedValueProjection(reached, source, joined, false);
if (stored !== null && stored?.length !== 0)
pendingStores.add(dependent);
}
}
yield;
}