[GH-ISSUE #6522] [Bug] Catch-shadowed var initializers lose their local call target #3915

Closed
opened 2026-10-31 00:45:12 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6522

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

Inside catch (handler), var handler = target; initializes the catch parameter at runtime, but REA attaches the initializer to the separate hoisted function variable. The following handler() is consequently unresolved with no callable candidates. Replacing only the declaration with handler = target; produces an exact target.

Observed through the production semantic analyzer and semantic graph builder, using the repository's single-file graph fixture. The declaration form produces no calls relation and a dynamic-call unknown whose detail is Call target handler has no local initializer.; the assignment control produces one resolved calls relation to target. Native Node.js execution of each exact module returns "target".

Reproduction

From the source checkout below with locked dependencies installed, save this probe outside the checkout as /tmp/rea-catch-var-probe.mts, then run node node_modules/tsx/dist/cli.mjs /tmp/rea-catch-var-probe.mts "$PWD". These are the executed graph-probe operations; the temporary script path is neutralized.

import { pathToFileURL } from 'node:url';
import { join } from 'node:path';
const repo=process.argv[2];
const { graphForJavaScript }=await import(pathToFileURL(join(repo,'tests/fixtures/javascriptSemanticGraph.ts')).href);
for(const declaration of ['var handler = target;', 'handler = target;']) {
 const source=`export function target() { return 'target'; }
export function run() {
  try { throw null; } catch (handler) {
    ${declaration}
    return handler();
  }
}`;
 const runtime=(await import('data:text/javascript,'+encodeURIComponent(source))).run();
 const graph=graphForJavaScript(source);
 console.log(JSON.stringify({declaration,runtime,coverage:graph.coverage,functions:graph.nodes.filter(n=>n.kind==='function'),relations:graph.relations.filter(r=>['calls','reads','defines','writes'].includes(r.relation)),unknowns:graph.unknowns}));
}

The initial module-level probe and graph probe each observed the same declaration/control distinction. No population-frequency estimate is available.

Expected behavior

The initializer must write the catch parameter while preserving the distinct hoisted var binding. ECMAScript B.3.4 explicitly distinguishes declaration instantiation from the initializer's assignment recipient. Both tested forms unconditionally assign the same local function before the call.

REA documents uniquely resolved local calls and preceding assignments dominating a read in the same conditional block. The declaration form should retain the same exact call target as the assignment control.

Impact and scope

For valid JavaScript that redeclares a simple catch parameter with an initialized var, REA loses a known local call and substitutes dynamic uncertainty in the semantic graph. This prevents a call-flow trace from following that target through the missing relation. The graph loss is directly observed; the downstream trace consequence follows from the missing relation and was not queried through CLI/MCP. Broader application frequency and other consumers are unmeasured.

REA version

Source revision e073f64be0a5a30c32d8b3cec23f9bd411080903, package version 6.7.0. The published npm package was not executed.

Runtime and platform

Node.js v26.12.1; Linux x86_64; locked tsx 4.23.15 and @babel/parser 8.0.4. No native analysis provider was used.

Target and artifact context

Self-authored JavaScript ES modules. Graph projection used a synthetic single-file app.js inventory through the existing test fixture; it did not register persistent Evidence or analyze a real application.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: ECMAScript B.3.4 and the REA workflow documentation linked above.
  • Failure source: The executed probe produced the declaration/control results below. The initializer-owner path is bindInnerDeclaration, semanticVariableScope, and addBinding.
  • Evidence provenance: observed.
  • Local verification: reproduced.
  • Reproduction completeness: complete for the source analyzer and graph boundary.
var handler = target;
  native module run(): "target"
  semantic call: resolution="unresolved", calleeCallableIds=[]
  graph calls relations: 0
  call unknown: "Call target handler has no local initializer."

handler = target;
  native module run(): "target"
  semantic call: resolution="exact", target callable="target"
  graph calls relations: 1, resolution="resolved"
  call unknowns: 0

The analyzer's catch binding has only its catch definition in the declaration case; the initialized variable definition appears in the separate function scope. The application analyzer reaches the same semantic implementation, and the graph call projector emits calls from its callable IDs. These entry-path statements are source-derived. End-to-end CLI/MCP execution and persistent Evidence retrieval were not run.

Typed outcome or recovery expected

Retain the local target in the declaration form and emit its resolved calls relation, while keeping the catch and hoisted variable identities distinct. Preserve the passing plain-assignment control.

Analysis or suspected cause

Source-derived: variable declarations pass both the declaration and initializer to bindPattern using the hoisted variable scope. semanticVariableScope climbs through catch scopes, and addBinding stores the initializer on that owner. This conflates declaration instantiation with lexical resolution of the initializer's write. The catch binding never receives that write.

Additional context

Open and closed searches used repo:morluto/rea is:issue is:open "catch" "var" and its is:closed counterpart, then the same state pair for "catch" "initializer", plus repo:morluto/rea is:issue "catch" "redeclaration" (no results) and repo:morluto/rea is:issue "catch parameter".

The closest results were closed #2562, about skipped initialization preserving undefined, and closed #2646, about logical assignments retaining an incoming value. This case instead assigns unconditionally to a different lexical owner. Closed #2890 concerns shadowed require classification; closed #2725 concerns a stale unknown-provenance test expectation. Open #6508 concerns future Promise initializers selected before execution. None describes this declaration/assignment ownership split.

NOT_ENOUGH_INFO: no last-known-good revision or real-application incidence was established. This report contains AI-assisted research with executed evidence labeled above.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A valid catch-shadowed var initializer loses a directly observed local call relation and introduces incorrect dynamic uncertainty; the equivalent plain assignment succeeds. The demonstrated scope is bounded static-analysis correctness.
  • Workaround status: unverified
  • Workaround basis: The syntactic control establishes the intended fact but changing analyzed target code is not an established workaround. No public option or alternative workflow preserving the retained graph was tested.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6522 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Evidence, artifacts, or reconstruction ## Observed behavior Inside `catch (handler)`, `var handler = target;` initializes the catch parameter at runtime, but REA attaches the initializer to the separate hoisted function variable. The following `handler()` is consequently unresolved with no callable candidates. Replacing only the declaration with `handler = target;` produces an exact target. Observed through the production semantic analyzer and semantic graph builder, using the repository's single-file graph fixture. The declaration form produces no `calls` relation and a `dynamic-call` unknown whose detail is `Call target handler has no local initializer.`; the assignment control produces one resolved `calls` relation to `target`. Native Node.js execution of each exact module returns `"target"`. ## Reproduction From the source checkout below with locked dependencies installed, save this probe outside the checkout as `/tmp/rea-catch-var-probe.mts`, then run `node node_modules/tsx/dist/cli.mjs /tmp/rea-catch-var-probe.mts "$PWD"`. These are the executed graph-probe operations; the temporary script path is neutralized. ```ts import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const repo=process.argv[2]; const { graphForJavaScript }=await import(pathToFileURL(join(repo,'tests/fixtures/javascriptSemanticGraph.ts')).href); for(const declaration of ['var handler = target;', 'handler = target;']) { const source=`export function target() { return 'target'; } export function run() { try { throw null; } catch (handler) { ${declaration} return handler(); } }`; const runtime=(await import('data:text/javascript,'+encodeURIComponent(source))).run(); const graph=graphForJavaScript(source); console.log(JSON.stringify({declaration,runtime,coverage:graph.coverage,functions:graph.nodes.filter(n=>n.kind==='function'),relations:graph.relations.filter(r=>['calls','reads','defines','writes'].includes(r.relation)),unknowns:graph.unknowns})); } ``` The initial module-level probe and graph probe each observed the same declaration/control distinction. No population-frequency estimate is available. ## Expected behavior The initializer must write the catch parameter while preserving the distinct hoisted `var` binding. [ECMAScript B.3.4](https://tc39.es/ecma262/multipage/additional-ecmascript-features-for-web-browsers.html#sec-variablestatements-in-catch-blocks) explicitly distinguishes declaration instantiation from the initializer's assignment recipient. Both tested forms unconditionally assign the same local function before the call. REA documents [uniquely resolved local calls and preceding assignments dominating a read in the same conditional block](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/docs/javascript-application-workflows.md#L177-L201). The declaration form should retain the same exact call target as the assignment control. ## Impact and scope For valid JavaScript that redeclares a simple catch parameter with an initialized `var`, REA loses a known local call and substitutes dynamic uncertainty in the semantic graph. This prevents a call-flow trace from following that target through the missing relation. The graph loss is directly observed; the downstream trace consequence follows from the missing relation and was not queried through CLI/MCP. Broader application frequency and other consumers are unmeasured. ## REA version Source revision `e073f64be0a5a30c32d8b3cec23f9bd411080903`, package version `6.7.0`. The published npm package was not executed. ## Runtime and platform Node.js `v26.12.1`; Linux x86_64; locked `tsx 4.23.15` and `@babel/parser 8.0.4`. No native analysis provider was used. ## Target and artifact context Self-authored JavaScript ES modules. Graph projection used a synthetic single-file `app.js` inventory through the existing test fixture; it did not register persistent Evidence or analyze a real application. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: ECMAScript B.3.4 and the REA workflow documentation linked above. - Failure source: The executed probe produced the declaration/control results below. The initializer-owner path is [bindInnerDeclaration](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/domain/javascript/javascriptSemanticAnalysis.ts#L1409-L1442), [semanticVariableScope](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/domain/javascript/javascriptSemanticState.ts#L508-L525), and [addBinding](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/domain/javascript/javascriptSemanticAnalysis.ts#L1929-L1945). - Evidence provenance: observed. - Local verification: reproduced. - Reproduction completeness: complete for the source analyzer and graph boundary. ```text var handler = target; native module run(): "target" semantic call: resolution="unresolved", calleeCallableIds=[] graph calls relations: 0 call unknown: "Call target handler has no local initializer." handler = target; native module run(): "target" semantic call: resolution="exact", target callable="target" graph calls relations: 1, resolution="resolved" call unknowns: 0 ``` The analyzer's catch binding has only its catch definition in the declaration case; the initialized variable definition appears in the separate function scope. The [application analyzer](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/application/javascript/JavaScriptArtifactAnalysis.ts#L419-L441) reaches the same semantic implementation, and the [graph call projector](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/application/javascript/JavaScriptSemanticGraphBuilder.ts#L875-L886) emits calls from its callable IDs. These entry-path statements are source-derived. End-to-end CLI/MCP execution and persistent Evidence retrieval were not run. ## Typed outcome or recovery expected Retain the local target in the declaration form and emit its resolved `calls` relation, while keeping the catch and hoisted variable identities distinct. Preserve the passing plain-assignment control. ## Analysis or suspected cause Source-derived: variable declarations pass both the declaration and initializer to `bindPattern` using the hoisted variable scope. `semanticVariableScope` climbs through catch scopes, and `addBinding` stores the initializer on that owner. This conflates declaration instantiation with lexical resolution of the initializer's write. The catch binding never receives that write. ## Additional context Open and closed searches used `repo:morluto/rea is:issue is:open "catch" "var"` and its `is:closed` counterpart, then the same state pair for `"catch" "initializer"`, plus `repo:morluto/rea is:issue "catch" "redeclaration"` (no results) and `repo:morluto/rea is:issue "catch parameter"`. The closest results were closed [#2562](https://github.com/morluto/rea/issues/2562), about skipped initialization preserving undefined, and closed [#2646](https://github.com/morluto/rea/issues/2646), about logical assignments retaining an incoming value. This case instead assigns unconditionally to a different lexical owner. Closed [#2890](https://github.com/morluto/rea/issues/2890) concerns shadowed require classification; closed [#2725](https://github.com/morluto/rea/issues/2725) concerns a stale unknown-provenance test expectation. Open [#6508](https://github.com/morluto/rea/issues/6508) concerns future Promise initializers selected before execution. None describes this declaration/assignment ownership split. NOT_ENOUGH_INFO: no last-known-good revision or real-application incidence was established. This report contains AI-assisted research with executed evidence labeled above. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A valid catch-shadowed var initializer loses a directly observed local call relation and introduces incorrect dynamic uncertainty; the equivalent plain assignment succeeds. The demonstrated scope is bounded static-analysis correctness. - Workaround status: unverified - Workaround basis: The syntactic control establishes the intended fact but changing analyzed target code is not an established workaround. No public option or alternative workflow preserving the retained graph was tested.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3915
No description provided.