diff --git a/src/domain/javascript/javascriptSemanticCallableResults.test.ts b/src/domain/javascript/javascriptSemanticCallableResults.test.ts new file mode 100644 index 0000000000..f2755e5e4e --- /dev/null +++ b/src/domain/javascript/javascriptSemanticCallableResults.test.ts @@ -0,0 +1,69 @@ +import * as t from "@babel/types"; +import { expect, it } from "vitest"; +import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js"; +import { parseJavaScriptSource } from "./javascriptSourceParser.js"; + +it("reuses direct result syntax while excluding nested callable results", () => { + const parsed = parseJavaScriptSource(`function* outer(){ + function inner(){return hidden;} + class Hidden{method(){return hidden;}} + if(flag)return first; + yield* second; + }`); + const callable = parsed?.program.body[0]; + if (!t.isFunctionDeclaration(callable)) + throw new Error("Expected callable fixture"); + const statements = callable.body.body; + let reads = 0; + Object.defineProperty(callable.body, "body", { + get: () => { + reads += 1; + return statements; + }, + }); + const result = semanticCallableResultExpressions(callable); + expect( + result.map(({ node, kind, delegated }) => [ + t.isIdentifier(node) ? node.name : null, + kind, + delegated, + ]), + ).toEqual([ + ["first", "return", false], + ["second", "yield", true], + ]); + expect(reads).toBeGreaterThan(0); + reads = 0; + for (let i = 0; i < 32; i++) + expect(semanticCallableResultExpressions(callable)).toBe(result); + expect(reads).toBe(0); +}); + +it("keeps identical source text in separate syntax trees distinct", () => { + const first = t.arrowFunctionExpression([], t.identifier("value")); + const second = t.arrowFunctionExpression([], t.identifier("value")); + expect(semanticCallableResultExpressions(first)[0]?.node).toBe(first.body); + expect(semanticCallableResultExpressions(second)[0]?.node).toBe(second.body); +}); + +it("retains empty results and bare return/yield source sites", () => { + const empty = t.functionExpression(null, [], t.blockStatement([])); + const emptyResult = semanticCallableResultExpressions(empty); + expect(emptyResult).toEqual([]); + expect(semanticCallableResultExpressions(empty)).toBe(emptyResult); + + const bareReturn = t.returnStatement(); + const bareYield = t.yieldExpression(); + const generator = t.functionExpression( + null, + [], + t.blockStatement([t.expressionStatement(bareYield), bareReturn]), + true, + ); + const result = semanticCallableResultExpressions(generator); + expect(result).toEqual([ + { node: null, site: bareYield, kind: "yield", delegated: false }, + { node: null, site: bareReturn, kind: "return", delegated: false }, + ]); + expect(semanticCallableResultExpressions(generator)).toBe(result); +}); diff --git a/src/domain/javascript/javascriptSemanticCallableResults.ts b/src/domain/javascript/javascriptSemanticCallableResults.ts index ba66a22790..d8ac7526f5 100644 --- a/src/domain/javascript/javascriptSemanticCallableResults.ts +++ b/src/domain/javascript/javascriptSemanticCallableResults.ts @@ -10,15 +10,25 @@ export interface SemanticCallableResultExpression { readonly delegated: boolean; } +// Syntax is immutable across the static and semantic passes. These are source +// expressions, independent of changing binding, mutation or invocation facts. +// Weak keys keep retained results within the lifetime of their parsed AST. +const resultExpressions = new WeakMap< + t.Node, + readonly SemanticCallableResultExpression[] +>(); + /** Collect direct result syntax without executing or evaluating a callable. */ export const semanticCallableResultExpressions = ( callable: t.Node, ): readonly SemanticCallableResultExpression[] => { + const cached = resultExpressions.get(callable); + if (cached !== undefined) return cached; if ( t.isArrowFunctionExpression(callable) && !t.isBlockStatement(callable.body) - ) - return [ + ) { + const result: readonly SemanticCallableResultExpression[] = [ { node: callable.body, site: callable.body, @@ -26,6 +36,9 @@ export const semanticCallableResultExpressions = ( delegated: false, }, ]; + resultExpressions.set(callable, result); + return result; + } if (!t.isFunction(callable)) return []; const output: SemanticCallableResultExpression[] = []; const pending = childNodes(callable.body).reverse(); @@ -49,6 +62,7 @@ export const semanticCallableResultExpressions = ( }); for (const child of childNodes(node).reverse()) pending.push(child); } + resultExpressions.set(callable, output); return output; };