From fba4faf88a58f674b89c814f56ece1add61b2bdf Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:28:12 +0800 Subject: [PATCH] fix(javascript): preserve initializer execution and lexical ownership --- docs/javascript-application-workflows.md | 10 + ...Analysis.initialization_boundaries.test.ts | 213 ++++++++++++++++++ .../javascript/javascriptSemanticAnalysis.ts | 146 +++++++++++- .../javascriptSemanticEvaluationOrder.ts | 93 ++++++++ .../javascriptSemanticExpressionShapes.ts | 14 +- .../javascript/javascriptSemanticPromises.ts | 26 +-- .../javascript/javascriptSemanticState.ts | 13 +- .../javascript/javascriptSemanticValues.ts | 14 +- 8 files changed, 488 insertions(+), 41 deletions(-) create mode 100644 src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md index 4fcafe45e9..7a09e4c6d8 100644 --- a/docs/javascript-application-workflows.md +++ b/docs/javascript-application-workflows.md @@ -374,6 +374,16 @@ Direct return expressions, including expression-bodied arrows, are evaluated through an execution-free value lattice. Literal object fields and direct return sites are represented in the result. Calls, dynamic spreads, computed keys, and parser recovery remain partial or unknown. +Declaration initializers and later assignments share the binding-at-read owner; +a declaration-only `var` read before its initializer does not inherit the future +value. Earlier `break`/`continue` paths can leave a var initializer unexecuted. +Legacy non-strict block-function copies are indexed on their outer variable +owner, while strict/module and lexical-shadowing boundaries keep their values. +A catch-shadowed var declaration keeps declaration ownership separate from its +initializer's lexical assignment. Ordinary functions own an implicit arguments +object; arrows retain lexical arguments lookup. Promise source identity uses +the same read-position selection and requires an unmodified intrinsic factory. + Rebound values are evaluated at their read position: saving an incoming parameter before a later assignment preserves caller-dependent uncertainty, while a proven preceding assignment can supply a constant. Unresolved ordering remains unknown. diff --git a/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts new file mode 100644 index 0000000000..f7cf8da8fc --- /dev/null +++ b/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts @@ -0,0 +1,213 @@ +import { expect, it } from "vitest"; +import { analyzeParsedJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { + onlyCallable, + onlyBinding, +} from "./javascriptSemanticAnalysis.fixture.js"; +import { parseJavaScriptSource } from "./javascriptSourceParser.js"; + +const analyze = (source: string, path = "fixture.cjs") => { + const file = parseJavaScriptSource(source, path); + if (file === null) throw new Error("Expected parsed fixture"); + return analyzeParsedJavaScriptSemantics(file); +}; +const returned = (body: string) => + onlyCallable(analyze(`exports.read = function read() { ${body} };`), "read") + .returnSites[0]?.value; + +it.each([ + "return value; var value = 1;", + "var snapshot = value; var value = 1; return snapshot;", +])("selects declaration-only bindings at their actual read: %s", (body) => { + expect(returned(body)?.status).toBe("unknown"); +}); + +it.each([ + "do { break; var value = 1; } while (false); return value;", + "do { if (flag) break; var value = 1; } while (false); return value;", + "do { continue; var value = 1; } while (false); return value;", + "label: { break label; var value = 1; } return value;", +])( + "does not treat an initializer skipped by abrupt completion as definite: %s", + (body) => { + expect(returned(body)?.status).toBe("unknown"); + }, +); + +it.each([ + "var value = 1; return value;", + "do { var value = 1; break; } while (false); return value;", + "do { for (;;) { break; } var value = 1; } while (false); return value;", + "label: { break label; } var value = 1; return value;", + "function unused() { return; } var value = 1; return value;", +])( + "preserves initializers that an earlier local exit does not skip: %s", + (body) => { + expect(returned(body)).toEqual({ status: "literal", value: 1 }); + }, +); + +it.each([ + "var value = 1; { function value() {} } exports.read = function read() { return value; };", + "exports.read = function read() { var value = 1; if (flag) { function value() {} } return value; };", +])( + "indexes the legacy block-function write on its outer owner: %s", + (source) => { + expect( + onlyCallable(analyze(source), "read").returnSites[0]?.value.status, + ).not.toBe("literal"); + }, +); + +it.each([ + [ + "fixture.cjs", + '"use strict"; var value = 1; { function value() {} } exports.read = function read() { return value; };', + ], + [ + "fixture.cjs", + "let value = 1; { function value() {} } exports.read = function read() { return value; };", + ], + [ + "fixture.mjs", + "var value = 1; { function value() {} } export function read() { return value; }", + ], +])( + "does not apply legacy writes across a strict or lexical boundary: %s", + (path, source) => { + expect( + onlyCallable(analyze(source, path), "read").returnSites[0]?.value, + ).toEqual({ status: "literal", value: 1 }); + }, +); + +it("assigns a catch-shadowed var initializer to the active lexical binding", () => { + const ir = analyze(` + function target() { return "target"; } + try { throw 0; } catch (handler) { var handler = target; handler(); } + `); + const target = onlyCallable(ir, "target"); + expect(ir.callSites).toContainEqual( + expect.objectContaining({ + resolution: "exact", + calleeCallableIds: [target.callableId], + }), + ); + const hoisted = ir.bindings.find( + ({ name, kind }) => name === "handler" && kind === "variable", + ); + expect(hoisted?.value.status).toBe("unknown"); +}); + +it("keeps ordinary arguments local while arrows retain the outer binding", () => { + const ir = analyze(` + var arguments = { 0: "OUTER" }; + function ordinary() { const selected = arguments[0]; return selected; } + const arrow = () => arguments[0]; + `); + expect(onlyBinding(ir, "selected").value.status).toBe("unknown"); + expect(onlyCallable(ir, "ordinary").returnSites[0]?.value.status).toBe( + "unknown", + ); + expect(onlyCallable(ir, "arrow").returnSites[0]?.value).toEqual({ + status: "literal", + value: "OUTER", + }); + const outer = onlyBinding(ir, "arguments"); + expect( + ir.closureCaptures.some( + ({ bindingId, callableId }) => + bindingId === outer.bindingId && + callableId === onlyCallable(ir, "ordinary").callableId, + ), + ).toBe(false); +}); + +it("does not resolve Promise sources through a future initializer", () => { + const early = analyze( + ` + try { task.then(work); } catch (error) {} + const task = Promise.resolve(1); + `, + "fixture.mjs", + ); + expect( + early.promiseOperations.find(({ method }) => method === "then"), + ).toMatchObject({ sourceResolution: "unresolved", sourcePromiseIds: [] }); + const initialized = analyze( + "const task = Promise.resolve(1); task.then(work);", + "fixture.mjs", + ); + expect( + initialized.promiseOperations.find(({ method }) => method === "then"), + ).toMatchObject({ + sourceResolution: "complete", + sourcePromiseIds: [expect.any(String)], + }); +}); + +it.each([ + "globalThis.Promise = { resolve() { return 17; } };", + "Promise.resolve = () => 17;", +])( + "requires the selected intrinsic Promise identity after ambient writes: %s", + (write) => { + const ir = analyze( + `${write} const task = Promise.resolve(1);`, + "fixture.mjs", + ); + expect( + ir.promiseOperations.filter(({ method }) => method === "resolve"), + ).toEqual([]); + expect( + analyze("const task = Promise.resolve(1);", "fixture.mjs") + .promiseOperations, + ).toContainEqual( + expect.objectContaining({ + method: "resolve", + sourceResolution: "complete", + }), + ); + }, +); + +it("retains caught reference identity without tainting inert throws or later replacements", () => { + const cases = [ + ["caught.value = 2;", "unknown"], + ["mutate(caught);", "unknown"], + ["", "literal"], + ["caught = {}; caught.value = 2;", "literal"], + ] as const; + for (const [body, status] of cases) + expect( + returned( + `const state = { value: 1 }; try { throw state; } catch (caught) { ${body} } return state.value;`, + )?.status, + ).toBe(status); +}); + +it("owns a class declaration's self reference separately from its outer variable", () => { + const ir = analyze( + ` + class Service { static create() { return new Service(); } } + const Original = Service; + Service = function Replacement() {}; + `, + "fixture.mjs", + ); + const bindings = ir.bindings.filter(({ name }) => name === "Service"); + expect(bindings).toHaveLength(2); + const create = onlyCallable(ir, "create"); + expect( + ir.closureCaptures.some( + ({ callableId, bindingId }) => + callableId === create.callableId && + bindingId === bindings.find(({ mutable }) => mutable)?.bindingId, + ), + ).toBe(false); + expect( + ir.callSites.find( + ({ callerCallableId }) => callerCallableId === create.callableId, + )?.resolution, + ).toBe("exact"); +}); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index b22047c67f..d6739d6e67 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -9,6 +9,7 @@ import * as t from "@babel/types"; import { semanticComputedMethodKey, semanticBranchMayBeSkipped, + semanticPriorStatementMayExit, semanticIterationCardinality, } from "./javascriptSemanticEvaluationOrder.js"; @@ -116,6 +117,7 @@ import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutio interface BindPatternInput { readonly iterationSources?: JavaScriptSemanticBindingState["initializers"][number]["iterationSources"]; readonly pattern: t.Node; + readonly entryBody?: t.BlockStatement; readonly initializer: t.Node | null; readonly scope: JavaScriptSemanticScopeState; readonly state: JavaScriptSemanticAnalysisState; @@ -1067,6 +1069,19 @@ function* collectDefinitionsSteps( node: t.CallExpression; scopeId: string; }[] = []; + const displacedVarInitializers: { + node: t.VariableDeclarator; + scope: JavaScriptSemanticScopeState; + owner: JavaScriptSemanticScopeState; + }[] = []; + const thrownValues: { + node: t.ThrowStatement; + ancestors: readonly t.Node[]; + }[] = []; + const legacyBlockFunctions: { + node: t.FunctionDeclaration; + scope: JavaScriptSemanticScopeState; + }[] = []; let needsPrototypeFacts = false; const functionBodies: { readonly body: t.BlockStatement; @@ -1117,6 +1132,21 @@ function* collectDefinitionsSteps( parentScope = dynamicScope; } bindOuterDeclaration(node, parentScope, state); + if ( + t.isFunctionDeclaration(node) && + node.id != null && + parentScope.kind === "block" + ) + legacyBlockFunctions.push({ node, scope: parentScope }); + if ( + t.isVariableDeclarator(node) && + node.init != null && + t.isIdentifier(node.id) + ) { + const owner = semanticVariableScope(parentScope, parent, state); + if (owner !== parentScope) + displacedVarInitializers.push({ node, scope: parentScope, owner }); + } const nested = nestedScope(node, parent, parentScope, state); if (nested !== undefined) { stack.push(nested); @@ -1141,6 +1171,8 @@ function* collectDefinitionsSteps( state, }); bindInnerDeclaration(node, parent, scope, state); + if (t.isThrowStatement(node)) + thrownValues.push({ node, ancestors: readAncestors() }); // Every supported global eval origin starts with one of these decoded // identifiers, including computed properties and destructured aliases. if (t.isIdentifier(node) && globalEvalRoots.has(node.name)) @@ -1180,6 +1212,94 @@ function* collectDefinitionsSteps( stack.pop(); }, }); + // A var declaration is instantiated in its variable environment, while its + // initializer assigns through the lexical environment at the source site. + for (const { node, scope, owner } of displacedVarInitializers) { + if (!t.isIdentifier(node.id) || node.init == null) continue; + const selected = resolveSemanticBindingFromScope( + scope, + node.id.name, + state, + ); + const declared = owner.bindings.get(node.id.name); + if ( + selected === undefined || + selected === declared || + declared === undefined + ) + continue; + for (let index = declared.initializers.length - 1; index >= 0; index--) + if (declared.initializers[index]?.node === node.init) + declared.initializers.splice(index, 1); + addAssignment( + node.id, + node.init, + scope, + state, + state.conditionallySkippedExpressions.has(node.init), + node, + ); + } + for (const { node, ancestors } of thrownValues) { + let child: t.Node = node; + for (let index = ancestors.length - 1; index >= 0; index--) { + const parent = ancestors[index]!; + if (t.isFunction(parent) || t.isClass(parent)) break; + if ( + t.isTryStatement(parent) && + parent.handler != null && + child === parent.block + ) { + const scope = state.scopeByNode.get(parent.handler); + if (scope !== undefined && parent.handler.param != null) + bindPattern({ + pattern: parent.handler.param, + initializer: node.argument, + scope, + state, + mutable: true, + referenceOnly: true, + entryBody: parent.handler.body, + }); + break; + } + child = parent; + } + } + for (const { node, scope } of legacyBlockFunctions) { + if ( + node.id == null || + semanticStrictnessAt(state.parentsByNode.get(node) ?? node, state) === + "strict" + ) + continue; + let outer = + scope.parentScopeId === null + ? undefined + : state.scopesById.get(scope.parentScopeId); + while (outer !== undefined) { + const binding = outer.bindings.get(node.id.name); + if ( + binding !== undefined && + !binding.declarationKinds.has("var") && + binding.kind !== "parameter" + ) + break; + if ( + outer.kind === "function" || + outer.kind === "program" || + outer.kind === "static-block" + ) { + if (binding !== undefined) + addAssignment(node.id, node, outer, state, false, node); + break; + } + outer = + outer.parentScopeId === null + ? undefined + : state.scopesById.get(outer.parentScopeId); + } + } if (needsPrototypeFacts) yield* indexPrototypeUncertaintySteps(program, state); // All declarations must exist before resolving writes, including hoisted @@ -1365,7 +1485,11 @@ const writeMayBeSkipped = ( t.isStaticBlock(parent) ) return false; - if (semanticBranchMayBeSkipped(parent, child)) return true; + if ( + semanticBranchMayBeSkipped(parent, child) || + semanticPriorStatementMayExit(parent, child) + ) + return true; child = parent; } return false; @@ -1421,7 +1545,7 @@ const bindInnerDeclaration = ( ? { declarationKind: parent.kind } : {}), }); - else if (t.isClassExpression(node) && t.isIdentifier(node.id)) + else if (t.isClass(node) && t.isIdentifier(node.id)) addBinding({ state, scope, @@ -1588,7 +1712,8 @@ const mappedArgumentsOwner = ( } if (!t.isIdentifier(value)) return undefined; - if (semanticBindingLookupAt(state, value, "arguments").kind !== "ambient") + const lookup = semanticBindingLookupAt(state, value, "arguments"); + if (lookup.kind !== "implicit-arguments" && lookup.kind !== "ambient") return undefined; let owner: t.Function | undefined; for ( @@ -1634,12 +1759,12 @@ const mappedArgumentsOwner = ( return { scope, parameters: mapped, - strictness: mappedArgumentsStrictness(owner, state), + strictness: semanticStrictnessAt(owner, state), }; }; -const mappedArgumentsStrictness = ( - owner: t.Function, +const semanticStrictnessAt = ( + owner: t.Node, state: JavaScriptSemanticAnalysisState, ): MappedArgumentsOwner["strictness"] => { for ( @@ -1737,6 +1862,9 @@ const nestedScope = ( location: range(switchOwner ?? node), bindingsComplete: true, bindings: new Map(), + ...(t.isFunction(node) && !t.isArrowFunctionExpression(node) + ? { implicitArguments: true as const } + : {}), }; state.scopes.push(scope); state.scopesById.set(scope.scopeId, scope); @@ -1914,6 +2042,9 @@ const bindPattern = (input: BindPatternInput): void => { binding.referenceInitializers.push({ node: initializer, projection, + ...(input.entryBody === undefined + ? {} + : { entryBody: input.entryBody }), ...(assignmentReference === undefined ? {} : { assignment: true }), ...(copyKind === undefined ? {} : { copyKind }), ...(copyProjectionOffset === undefined @@ -2136,7 +2267,8 @@ const addAssignment = ( const mutation = writeSite ?? initializer; if (mutation === null) return; const lookup = semanticBindingLookupFromScope(scope, identifier.name, state); - if (lookup.kind === "captured") return; + if (lookup.kind === "captured" || lookup.kind === "implicit-arguments") + return; const binding = "binding" in lookup ? lookup.binding : undefined; conditionallySkipped ||= lookup.kind !== "binding" && lookup.kind !== "ambient"; diff --git a/src/domain/javascript/javascriptSemanticEvaluationOrder.ts b/src/domain/javascript/javascriptSemanticEvaluationOrder.ts index d5cdc38c5a..97bc10d8e9 100644 --- a/src/domain/javascript/javascriptSemanticEvaluationOrder.ts +++ b/src/domain/javascript/javascriptSemanticEvaluationOrder.ts @@ -6,6 +6,8 @@ import { unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; +import { childNodes } from "./javascriptSemanticTraversal.js"; + const preserveGrouping = { stopAtParentheses: true } as const; export type JavaScriptSemanticIterationCardinality = @@ -155,3 +157,94 @@ const optionalChainMayShortCircuit = (expression: t.Node): boolean => { else return false; } }; + +interface AbruptCompletionSummary { + readonly breaks: ReadonlySet; + readonly continues: ReadonlySet; +} +const emptyAbruptLabels: ReadonlySet = new Set(); +const emptyAbruptSummary: AbruptCompletionSummary = { + breaks: emptyAbruptLabels, + continues: emptyAbruptLabels, +}; +const abruptSummaries = new WeakMap(); +const abruptPrefixes = new WeakMap< + t.BlockStatement | t.Program, + ReadonlySet +>(); + +/** Loop and label exits crossing a statement boundary, excluding nested callables. */ +const abruptSummary = (root: t.Node): AbruptCompletionSummary => { + const pending: { node: t.Node; visited: boolean }[] = [ + { node: root, visited: false }, + ]; + while (pending.length > 0) { + const { node, visited } = pending.pop()!; + if (abruptSummaries.has(node)) continue; + const children = + t.isFunction(node) || t.isClass(node) + ? [] + : childNodes(node).filter( + (child) => + t.isStatement(child) || + t.isSwitchCase(child) || + t.isCatchClause(child), + ); + if (!visited) { + pending.push({ node, visited: true }); + for (const child of children) + if (!abruptSummaries.has(child)) + pending.push({ node: child, visited: false }); + continue; + } + let breaks: Set | undefined, + continues: Set | undefined; + if (t.isBreakStatement(node)) + (breaks ??= new Set()).add(node.label?.name ?? null); + if (t.isContinueStatement(node)) + (continues ??= new Set()).add(node.label?.name ?? null); + for (const child of children) { + const summary = abruptSummaries.get(child)!; + for (const label of summary.breaks) (breaks ??= new Set()).add(label); + for (const label of summary.continues) + (continues ??= new Set()).add(label); + } + if (t.isLoop(node) || t.isSwitchStatement(node)) breaks?.delete(null); + if (t.isLoop(node)) continues?.delete(null); + if (t.isLabeledStatement(node)) { + breaks?.delete(node.label.name); + continues?.delete(node.label.name); + } + abruptSummaries.set( + node, + (breaks?.size ?? 0) > 0 || (continues?.size ?? 0) > 0 + ? { + breaks: breaks ?? emptyAbruptLabels, + continues: continues ?? emptyAbruptLabels, + } + : emptyAbruptSummary, + ); + } + return abruptSummaries.get(root)!; +}; + +/** An earlier sibling may leave this body before reaching the selected statement. */ +export const semanticPriorStatementMayExit = ( + parent: t.Node, + child: t.Node, +): boolean => { + if (!t.isBlockStatement(parent) && !t.isProgram(parent)) return false; + let skipped = abruptPrefixes.get(parent); + if (skipped === undefined) { + const prefixes = new Set(); + let exits = false; + for (const statement of parent.body) { + if (exits) prefixes.add(statement); + const summary = abruptSummary(statement); + exits ||= summary.breaks.size > 0 || summary.continues.size > 0; + } + skipped = prefixes; + abruptPrefixes.set(parent, skipped); + } + return skipped.has(child); +}; diff --git a/src/domain/javascript/javascriptSemanticExpressionShapes.ts b/src/domain/javascript/javascriptSemanticExpressionShapes.ts index 3cc73ac0e9..db2ce9d067 100644 --- a/src/domain/javascript/javascriptSemanticExpressionShapes.ts +++ b/src/domain/javascript/javascriptSemanticExpressionShapes.ts @@ -53,7 +53,6 @@ export const createSemanticExpressionShapeResolver = ( // Most modules have no named child arguments. Collect source-wide blockers // only when a query actually needs a named binding or ambient undefined. let blocked: boolean | undefined; - const blockFunctionNames = new Set(); const namedProofBlocked = (): boolean => { if (blocked !== undefined) return blocked; blocked = false; @@ -62,13 +61,6 @@ export const createSemanticExpressionShapeResolver = ( blocked ||= isErasedJavaScriptDeclaration(node) || isUnmodeledJavaScriptRuntimeScope(node); - if ( - javaScriptRuntimeContext(state.program) !== "module" && - t.isFunctionDeclaration(node) && - node.id != null && - !hoistedFunction(node) - ) - blockFunctionNames.add(node.id.name); }, }); return blocked; @@ -105,13 +97,9 @@ export const createSemanticExpressionShapeResolver = ( ) return "unknown"; } - // Annex B may copy a block function into an outer var without an indexed - // assignment. Keep same-named vars unknown rather than model that copy. if ( namedProofBlocked() || - semanticOpaqueBindingReadMayHaveChanged(binding, node, state) || - (binding.declarationKinds.has("var") && - blockFunctionNames.has(binding.name)) + semanticOpaqueBindingReadMayHaveChanged(binding, node, state) ) return "unknown"; const initializer = semanticStableInitializer(binding, state); diff --git a/src/domain/javascript/javascriptSemanticPromises.ts b/src/domain/javascript/javascriptSemanticPromises.ts index 5178c04dff..476d1c1feb 100644 --- a/src/domain/javascript/javascriptSemanticPromises.ts +++ b/src/domain/javascript/javascriptSemanticPromises.ts @@ -13,11 +13,12 @@ import { } from "./javascriptAstValues.js"; import { resolveSemanticBindingState, - semanticResolutionBlocked, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; +import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; import { assignedSemanticResultBindings, @@ -140,7 +141,7 @@ const promiseCandidateDetails = ( if ( t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Promise" }) && - isUnshadowedGlobalPromise(node.callee, state) + isUnmodifiedGlobalReference(node.callee, state, "Promise") ) return { kind: "constructor", method: "new" }; if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) { @@ -149,7 +150,7 @@ const promiseCandidateDetails = ( const method = semanticStaticPropertyName(member.property, member.computed); if ( t.isIdentifier(member.object, { name: "Promise" }) && - isUnshadowedGlobalPromise(member.object, state) + isUnmodifiedGlobalReference(node.callee, state, ["Promise", method ?? ""]) ) { if (method === "all" || method === "allSettled") return { kind: "aggregate", method }; @@ -164,13 +165,6 @@ const promiseCandidateDetails = ( return null; }; -const isUnshadowedGlobalPromise = ( - node: t.Identifier, - state: JavaScriptSemanticAnalysisState, -): boolean => - resolveSemanticBindingState(state, node, "Promise") === undefined && - !semanticResolutionBlocked(state, node, "Promise"); - const isPromiseProducer = ( node: t.Node, state: JavaScriptSemanticAnalysisState, @@ -447,14 +441,16 @@ const resolvePromiseExpression = ( return { promiseIds: [direct.promiseId], status: "complete" }; if (!t.isIdentifier(node)) return unresolvedPromise(); const binding = resolveSemanticBindingState(state, node, node.name); + if (binding === undefined || seenBindings.has(binding.bindingId)) + return unresolvedPromise(); + const read = semanticBindingAtRead(binding, node, state.parentsByNode); if ( - binding === undefined || - binding.mutable || - binding.initializers.length !== 1 || - seenBindings.has(binding.bindingId) + read.status !== "ordered" || + !read.hasDefiniteInitializer || + read.binding.initializers.length !== 1 ) return unresolvedPromise(); - const initializer = binding.initializers[0]; + const initializer = read.binding.initializers[0]; if (initializer === undefined || initializer.projection.length > 0) return unresolvedPromise(); const nestedSeen = new Set(seenBindings); diff --git a/src/domain/javascript/javascriptSemanticState.ts b/src/domain/javascript/javascriptSemanticState.ts index e8af7d3a58..ce536d5489 100644 --- a/src/domain/javascript/javascriptSemanticState.ts +++ b/src/domain/javascript/javascriptSemanticState.ts @@ -117,6 +117,8 @@ export interface JavaScriptSemanticScopeState { readonly bindings: Map; /** Object environment introduced by `with`, active only for its body. */ readonly dynamicWithStatement?: t.WithStatement; + /** Ordinary functions own `arguments`; arrow functions inherit it. */ + readonly implicitArguments?: true; } /** Shared state for semantic collection and evaluation. */ @@ -301,7 +303,14 @@ type SemanticBindingLookup = readonly kind: "binding" | "uncertain-binding"; readonly binding: JavaScriptSemanticBindingState; } - | { readonly kind: "ambient" | "uncertain-ambient" | "captured" | "unknown" }; + | { + readonly kind: + | "ambient" + | "uncertain-ambient" + | "captured" + | "unknown" + | "implicit-arguments"; + }; /** Retain both a dynamic lookup's uncertainty and its possible outer write owner. */ export const semanticBindingLookupFromScope = ( @@ -314,6 +323,8 @@ export const semanticBindingLookupFromScope = ( const binding = scope.bindings.get(name); if (binding !== undefined) return { kind: uncertain ? "uncertain-binding" : "binding", binding }; + if (name === "arguments" && scope.implicitArguments === true) + return { kind: "implicit-arguments" }; if (!scope.bindingsComplete) { if (scope.dynamicWithStatement === undefined) return { kind: "unknown" }; const lookup = semanticWithRequireLookup( diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 50d0f8714a..b554120423 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -658,12 +658,16 @@ const evaluateExpressionValue = ( const binding = resolveSemanticBindingState(context.state, node, node.name); if (binding === undefined) return { status: "unknown", reason: `Unbound identifier ${node.name}.` }; - const read = + const read = semanticBindingAtRead( + binding, + node, + context.state.parentsByNode, + ); + if ( + read.status === "unresolved" && binding.mutable && binding.definitions.some(({ kind }) => kind === "assignment") - ? semanticBindingAtRead(binding, node, context.state.parentsByNode) - : undefined; - if (read?.status === "unresolved") { + ) { const value = evaluateBinding(binding, nestedContext(context)); if ( value.status === "unknown" || @@ -676,7 +680,7 @@ const evaluateExpressionValue = ( reason: `Binding ${binding.name} has unresolved ordering at this read.`, }; } - const atRead = read?.binding ?? binding; + const atRead = read.binding; if ( semanticOpaqueBindingReadMayHaveChanged( binding,