From 2cedde9f1cf72b04bef37f9f004bb68d5ff6502a Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:53:41 +0800 Subject: [PATCH] perf(javascript): index Promise return-site ownership (#971) * perf(javascript): index promise return ownership lookups * perf(javascript): defer Promise return-site indexing --- ...SemanticAnalysis.promise_ownership.test.ts | 51 ++++++++++++ .../javascript/javascriptSemanticPromises.ts | 82 ++++++++++++++----- 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts index 66d484506..58b572a96 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.promise_ownership.test.ts @@ -147,3 +147,54 @@ it("keeps an explicit return statement's full range and the producer's location" }); expect(site.location.start.column).toBe(source.indexOf("return")); }); + +it("links returned promises to the exact return site in each callable", () => { + const ir = analyzeJavaScriptSemantics( + [ + "function first() { return Promise.resolve(1); }", + "function second() { return Promise.resolve(2); }", + ].join("\n"), + ); + const callables = new Map( + ir.callables.map((callable) => [callable.name, callable]), + ); + const promises = ir.promiseOperations.filter( + ({ method }) => method === "resolve", + ); + + expect(promises).toHaveLength(2); + for (const name of ["first", "second"] as const) { + const callable = callables.get(name); + const promise = promises.find( + ({ ownerCallableId }) => ownerCallableId === callable?.callableId, + ); + expect(callable?.returnSites).toHaveLength(1); + expect(promise).toMatchObject({ + ownership: "returned", + ownerCallableId: callable?.callableId, + returnSiteId: callable?.returnSites[0]?.returnSiteId, + }); + } +}); + +it("does not assign a return site to a Promise owned by a binding", () => { + const ir = analyzeJavaScriptSemantics( + "function unrelated() { return Promise.resolve(2); }\nconst pending = Promise.resolve(1);", + ); + const owned = ir.promiseOperations.find( + ({ ownerCallableId }) => ownerCallableId !== null, + ); + const assigned = ir.promiseOperations.find( + ({ ownerCallableId }) => ownerCallableId === null, + ); + + expect(owned).toMatchObject({ + ownership: "returned", + returnSiteId: ir.callables.find(({ name }) => name === "unrelated") + ?.returnSites[0]?.returnSiteId, + }); + expect(assigned).toMatchObject({ + ownership: "assigned", + returnSiteId: null, + }); +}); diff --git a/src/domain/javascript/javascriptSemanticPromises.ts b/src/domain/javascript/javascriptSemanticPromises.ts index 873d3491e..31510e6c8 100644 --- a/src/domain/javascript/javascriptSemanticPromises.ts +++ b/src/domain/javascript/javascriptSemanticPromises.ts @@ -15,7 +15,7 @@ import { } from "./javascriptSemanticState.js"; import { compareCodePoints } from "../canonicalOrdering.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; -import { range, sourceRangesEqual } from "./javascriptStaticAnalysisHelpers.js"; +import { range } from "./javascriptStaticAnalysisHelpers.js"; import { assignedSemanticResultBindings, dataEffectMemberCallee, @@ -56,7 +56,10 @@ interface PromiseOwnershipContext { readonly candidate: PromiseCandidate; readonly candidateByNode: WeakMap; readonly state: JavaScriptSemanticAnalysisState; - readonly callables: readonly JavaScriptSemanticCallable[]; + readonly returnSiteIdFor: ( + callableId: string | null, + location: JavaScriptSemanticPromiseOperation["location"], + ) => string | null; } /** Recover explicit Promise construction, chaining, aggregation, and ownership. */ @@ -66,6 +69,7 @@ export const collectJavaScriptSemanticPromises = ( callables: readonly JavaScriptSemanticCallable[], ): JavaScriptSemanticPromiseOperation[] => { const candidates = collectCandidates(program, state, callables); + const returnSiteIdFor = createReturnSiteLookup(callables); const candidateByNode = new WeakMap(); for (const candidate of candidates) candidateByNode.set(candidate.node, candidate); @@ -74,7 +78,7 @@ export const collectJavaScriptSemanticPromises = ( candidate, candidateByNode, state, - callables, + returnSiteIdFor, ); const sources = promiseSources(candidate, candidateByNode, state); return { @@ -180,14 +184,14 @@ const promiseOwnership = ( candidate: PromiseCandidate, candidateByNode: WeakMap, state: JavaScriptSemanticAnalysisState, - callables: readonly JavaScriptSemanticCallable[], + returnSiteIdFor: PromiseOwnershipContext["returnSiteIdFor"], ): PromiseOwnership => { if (candidate.kind === "awaited-expression") return emptyOwnership("awaited"); const context: PromiseOwnershipContext = { candidate, candidateByNode, state, - callables, + returnSiteIdFor, }; for (let index = candidate.ancestors.length - 1; index >= 0; index -= 1) { const ancestor = candidate.ancestors[index]; @@ -203,7 +207,7 @@ const ownershipAtAncestor = ( ancestor: t.Node, context: PromiseOwnershipContext, ): PromiseOwnership | "boundary" | null => { - const { candidate, candidateByNode, state, callables } = context; + const { candidate, candidateByNode, state, returnSiteIdFor } = context; if (t.isAwaitExpression(ancestor)) return unwrapExpression(ancestor.argument) === candidate.node ? emptyOwnership("awaited") @@ -234,7 +238,7 @@ const ownershipAtAncestor = ( return returnedPromiseOwnership( candidate.ownerCallableId, range(ancestor), - callables, + returnSiteIdFor, ); } if (t.isExpressionStatement(ancestor)) @@ -249,7 +253,7 @@ const ownershipAtAncestor = ( ? returnedPromiseOwnership( candidate.ownerCallableId, range(ancestor.body), - callables, + returnSiteIdFor, ) : "boundary"; }; @@ -283,11 +287,11 @@ const assignedPromiseBinding = ( const returnedPromiseOwnership = ( callableId: string | null, location: JavaScriptSemanticPromiseOperation["location"], - callables: readonly JavaScriptSemanticCallable[], + returnSiteIdFor: PromiseOwnershipContext["returnSiteIdFor"], ): PromiseOwnership => ({ ownership: "returned", ownerBindingId: null, - returnSiteId: matchingReturnSite(callableId, location, callables), + returnSiteId: returnSiteIdFor(callableId, location), }); const outerConsumesCandidate = ( @@ -329,16 +333,56 @@ const emptyOwnership = ( returnSiteId: null, }); -const matchingReturnSite = ( - callableId: string | null, - location: JavaScriptSemanticPromiseOperation["location"], +const indexReturnSites = ( + callable: JavaScriptSemanticCallable, +): ReadonlyMap => { + const output = new Map(); + for (const site of callable.returnSites) { + const key = returnSiteKey(site.location); + if (!output.has(key)) output.set(key, site.returnSiteId); + } + return output; +}; + +const createReturnSiteLookup = ( callables: readonly JavaScriptSemanticCallable[], -): string | null => - callables - .find(({ callableId: candidate }) => candidate === callableId) - ?.returnSites.find(({ location: candidate }) => - sourceRangesEqual(candidate, location), - )?.returnSiteId ?? null; +): PromiseOwnershipContext["returnSiteIdFor"] => { + let callablesById: + | ReadonlyMap + | undefined; + let returnSiteIdsByCallable: + | Map> + | undefined; + return (callableId, location) => { + if (callableId === null) return null; + callablesById ??= indexCallablesById(callables); + const callable = callablesById.get(callableId); + if (callable === undefined) return null; + returnSiteIdsByCallable ??= new Map(); + let returnSiteIds = returnSiteIdsByCallable.get(callableId); + if (returnSiteIds === undefined) { + returnSiteIds = indexReturnSites(callable); + returnSiteIdsByCallable.set(callableId, returnSiteIds); + } + return returnSiteIds.get(returnSiteKey(location)) ?? null; + }; +}; + +const indexCallablesById = ( + callables: readonly JavaScriptSemanticCallable[], +): ReadonlyMap => { + const output = new Map(); + for (const callable of callables) { + if (output.has(callable.callableId)) continue; + output.set(callable.callableId, callable); + } + return output; +}; + +const returnSiteKey = ( + location: JavaScriptSemanticPromiseOperation["location"], +): string => + `${location.start.line}:${location.start.column}:${location.end.line}:${location.end.column}`; const promiseSources = ( candidate: PromiseCandidate,