diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 7b988af889..167e8899e4 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -54,6 +54,57 @@ export function* collectSemanticMemberMutationsSteps( const selections = new Map(); const coveringPaths = new Map(); let arrayIterationUnknown = false; + // Definition collection finishes before this phase, so a scope's bindings no + // longer change and one node resolves to one binding. Repeated visits of the + // same node otherwise repeat the same scope walk. + const resolvedBindings = new WeakMap< + t.Node, + JavaScriptSemanticBindingState | null + >(); + const resolveBinding = ( + node: t.Node, + name: string, + ): JavaScriptSemanticBindingState | undefined => { + const cached = resolvedBindings.get(node); + if (cached !== undefined) return cached ?? undefined; + const resolved = resolveSemanticBindingState(state, node, name); + resolvedBindings.set(node, resolved ?? null); + return resolved; + }; + // One capture tuple recurs across every alias it reaches. Re-encoding it + // would rebuild the same long key and force a fresh string hash per lookup. + const captureKeys = new WeakMap< + JavaScriptSemanticBindingState, + Map + >(); + const captureKeyOf = ( + binding: JavaScriptSemanticBindingState, + originAt: t.Node | undefined, + ownerInitializer: t.Node | undefined, + ): string => { + let byPosition = captureKeys.get(binding); + if (byPosition === undefined) { + byPosition = new Map(); + captureKeys.set(binding, byPosition); + } + const positionKey = `${originAt?.start ?? "u"}:${originAt?.end ?? "u"}:${ + ownerInitializer?.start ?? "u" + }:${ownerInitializer?.end ?? "u"}`; + const cached = byPosition.get(positionKey); + if (cached !== undefined) return cached; + const created = JSON.stringify([ + binding.bindingId, + originAt?.start, + originAt?.end, + ownerInitializer?.start, + ownerInitializer?.end, + ]); + byPosition.set(positionKey, created); + return created; + }; + // A capture key already ends the array; a path stays one nested element. + const lifetimeKey = (captureKey: string, path: PropertyPath): string => + `${captureKey.slice(0, -1)},${JSON.stringify(path)}]`; const pendingReferences: { readonly initializer: JavaScriptSemanticBindingState["referenceInitializers"][number]; readonly path: PropertyPath; @@ -121,11 +172,7 @@ export function* collectSemanticMemberMutationsSteps( if (current === undefined) break; const expression = unwrapJavaScriptExpression(current.node).node; if (t.isIdentifier(expression)) { - const binding = resolveSemanticBindingState( - state, - expression, - expression.name, - ); + const binding = resolveBinding(expression, expression.name); if (binding === undefined) { if ( (expression.name === "Array" && @@ -142,21 +189,18 @@ export function* collectSemanticMemberMutationsSteps( // An alias restored from a saved reference can revisit the same binding // at an earlier capture or a shorter getter-receiver path. Only a // repeated capture and path closes a cycle. - const capture = [ - binding.bindingId, - current.originAt?.start, - current.originAt?.end, - current.ownerInitializer?.start, - current.ownerInitializer?.end, - ]; - const lifetime = JSON.stringify([...capture, current.path]); + const captureKey = captureKeyOf( + binding, + current.originAt, + current.ownerInitializer, + ); + const lifetime = lifetimeKey(captureKey, current.path); if (current.bindings.has(lifetime)) continue; // A self alias such as `node = node[key]` revisits its capture with the // same prefix added on every cycle, so exact paths never repeat. When // that prefix can never be pruned, escaping it once covers every longer // effect, including writes below shared children. // Only a capture already on this alias chain can repeat a prefix. - const captureKey = JSON.stringify(capture); const revisited = current.bindings.has(captureKey); let repeatedPrefix: PropertyPath | undefined; for ( @@ -165,8 +209,7 @@ export function* collectSemanticMemberMutationsSteps( start++ ) { const suffix = current.path.slice(start); - if (!current.bindings.has(JSON.stringify([...capture, suffix]))) - continue; + if (!current.bindings.has(lifetimeKey(captureKey, suffix))) continue; repeatedPrefix = current.path.slice(0, start); break; } @@ -408,7 +451,7 @@ export function* collectSemanticMemberMutationsSteps( if (!t.isNode(node)) continue; const pathKey = JSON.stringify(value.path); if (t.isIdentifier(node) && value.iterableFallbackPath === undefined) { - const binding = resolveSemanticBindingState(state, node, node.name); + const binding = resolveBinding(node, node.name); const identity = JSON.stringify([ binding === undefined ? null : binding.bindingId, binding === undefined ? node.name : null, diff --git a/src/domain/javascript/javascriptSemanticMutationInitializers.ts b/src/domain/javascript/javascriptSemanticMutationInitializers.ts index 6de7217c1a..414d3e26da 100644 --- a/src/domain/javascript/javascriptSemanticMutationInitializers.ts +++ b/src/domain/javascript/javascriptSemanticMutationInitializers.ts @@ -12,6 +12,37 @@ interface StatementPosition { readonly declaratorIndex: number; } +/** + * Initializer positions depend only on a node and its parent map, which one + * analysis builds once and never changes. Different mutations and initializers + * re-derive the same positions along shared ancestry, so retain the first + * result for as long as its parent map is reachable. + */ +interface InitializerPositionCache { + readonly direct: WeakMap; + readonly mutation: WeakMap; + readonly reference: WeakMap; +} + +const initializerPositionCaches = new WeakMap< + WeakMap, + InitializerPositionCache +>(); + +const initializerPositionCache = ( + parents: WeakMap, +): InitializerPositionCache => { + const cached = initializerPositionCaches.get(parents); + if (cached !== undefined) return cached; + const created: InitializerPositionCache = { + direct: new WeakMap(), + mutation: new WeakMap(), + reference: new WeakMap(), + }; + initializerPositionCaches.set(parents, created); + return created; +}; + const statementPosition = ( statement: t.Statement, parents: WeakMap, @@ -42,7 +73,7 @@ const positionPrecedes = ( left.declaratorIndex >= 0 && left.declaratorIndex < right.declaratorIndex)); -const directInitializerPosition = ( +const computeDirectInitializerPosition = ( node: t.Node, parents: WeakMap, ): StatementPosition | null => { @@ -67,7 +98,19 @@ const directInitializerPosition = ( return null; }; -const directMutationPosition = ( +const directInitializerPosition = ( + node: t.Node, + parents: WeakMap, +): StatementPosition | null => { + const cache = initializerPositionCache(parents); + const cached = cache.direct.get(node); + if (cached !== undefined) return cached; + const computed = computeDirectInitializerPosition(node, parents); + cache.direct.set(node, computed); + return computed; +}; + +const computeDirectMutationPosition = ( node: t.Node, parents: WeakMap, ): StatementPosition | null => { @@ -115,7 +158,19 @@ const directMutationPosition = ( } }; -const referenceInitializerPosition = ( +const directMutationPosition = ( + node: t.Node, + parents: WeakMap, +): StatementPosition | null => { + const cache = initializerPositionCache(parents); + const cached = cache.mutation.get(node); + if (cached !== undefined) return cached; + const computed = computeDirectMutationPosition(node, parents); + cache.mutation.set(node, computed); + return computed; +}; + +const computeReferenceInitializerPosition = ( node: t.Node, parents: WeakMap, ): StatementPosition | null => { @@ -137,6 +192,18 @@ const referenceInitializerPosition = ( } }; +const referenceInitializerPosition = ( + node: t.Node, + parents: WeakMap, +): StatementPosition | null => { + const cache = initializerPositionCache(parents); + const cached = cache.reference.get(node); + if (cached !== undefined) return cached; + const computed = computeReferenceInitializerPosition(node, parents); + cache.reference.set(node, computed); + return computed; +}; + /** Resolve origins at their use or capture, never at a later transitive effect. */ export const semanticMutationInitializers = ( binding: JavaScriptSemanticBindingState,