[GH-ISSUE #3261] JavaScript identifier default exports lose stable callable return shapes #1167

Closed
opened 2026-10-15 18:31:40 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @xu91102 on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3261

Reproduction

On main 29d3dcb7ea85b3be37cd81e977ae4e09cebdd2fc, with Node 24.18.0/npm 11.16.0 and locked dependencies, create two small application directories whose entry.mjs differs only in answer: 42 versus answer: 43:

const run = () => ({ type: "result", answer: 42 });
export default run;

Node imports each module's default function and returns the corresponding object. Analyze both with rea analyze-javascript-application <directory> --json, then compare the saved Evidence using rea compare-javascript-export-shapes <input.json> --json, selecting entry.mjs and export default on both sides.

The actual public CLI reports both selections unavailable, changed: 0, unknown: 1, and partial coverage. It emits no export return-shape observation. All commands finish successfully; this is missing analysis, not a failed loader or malformed input.

Expected behavior and control

The stable local callable should remain associated with its default export so the supported static return-shape workflow can report /answer changing from 42 to 43.

Changing only the export syntax to export { run as default } makes both selectors selected, with /answer changed and unknown: 0. Native Node returns the same function results for this stable const control. Exact lexical/module associations and expression-bodied arrow return fields are covered by the existing JavaScript application workflow documentation.

Producing boundary and scope

javascriptSemanticProjection collects declaration-default names but does not retain an identifier default expression's lexical binding. The existing callable resolver consequently receives neither the binding nor a directly exported callable. This should be repaired at the module-link producer and reuse the existing lexical resolution owner.

Keep ambiguity and reassignment controls conservative: a default expression captures its value at evaluation, whereas a named export follows its binding. The fix must not turn a reassigned or conditionally initialized binding into an exact live default-export association. See the ECMAScript export evaluation contract.

Verified with real Node fixture modules and the compiled public CLI on macOS arm64. Windows/Linux execution and a repair are not yet verified. Existing issue/PR searches and relevant export-resolution diffs were checked before filing; no implementation for this exact stable identifier case was found.

Originally created by @xu91102 on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3261 ## Reproduction On main `29d3dcb7ea85b3be37cd81e977ae4e09cebdd2fc`, with Node 24.18.0/npm 11.16.0 and locked dependencies, create two small application directories whose `entry.mjs` differs only in `answer: 42` versus `answer: 43`: ```js const run = () => ({ type: "result", answer: 42 }); export default run; ``` Node imports each module's default function and returns the corresponding object. Analyze both with `rea analyze-javascript-application <directory> --json`, then compare the saved Evidence using `rea compare-javascript-export-shapes <input.json> --json`, selecting `entry.mjs` and export `default` on both sides. The actual public CLI reports both selections `unavailable`, `changed: 0`, `unknown: 1`, and partial coverage. It emits no export return-shape observation. All commands finish successfully; this is missing analysis, not a failed loader or malformed input. ## Expected behavior and control The stable local callable should remain associated with its default export so the supported static return-shape workflow can report `/answer` changing from 42 to 43. Changing only the export syntax to `export { run as default }` makes both selectors `selected`, with `/answer` changed and `unknown: 0`. Native Node returns the same function results for this stable `const` control. Exact lexical/module associations and expression-bodied arrow return fields are covered by the existing JavaScript application workflow documentation. ## Producing boundary and scope `javascriptSemanticProjection` collects declaration-default names but does not retain an identifier default expression's lexical binding. The existing callable resolver consequently receives neither the binding nor a directly exported callable. This should be repaired at the module-link producer and reuse the existing lexical resolution owner. Keep ambiguity and reassignment controls conservative: a default expression captures its value at evaluation, whereas a named export follows its binding. The fix must not turn a reassigned or conditionally initialized binding into an exact live default-export association. See the [ECMAScript export evaluation contract](https://tc39.es/ecma262/2025/multipage/ecmascript-language-scripts-and-modules.html#sec-exports-runtime-semantics-evaluation). Verified with real Node fixture modules and the compiled public CLI on macOS arm64. Windows/Linux execution and a repair are not yet verified. Existing issue/PR searches and relevant export-resolution diffs were checked before filing; no implementation for this exact stable identifier case was found.
bsakaguchi 2026-10-15 18:31:40 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@CRYPTONIKAV commented on GitHub (Oct 14, 2026):

Traced this to defaultDeclarationName and have a minimal fix. PR creation from my fork is currently repository-blocked (REST 404), so leaving the patch here:

Root cause: for export default run, defaultDeclarationName returns null (it only names Function/Class declarations), so the link records no localName and resolveSemanticModuleCallables never follows the lexical binding — callableId stays null and the return-shape projection bails. The export { run as default } control works because the specifier path records localName and the resolver follows it.

Fix (src/domain/javascript/javascriptSemanticProjection.ts): a bare identifier keeps its name as localName; binding/initializer/callable resolution then runs the exact pre-existing path. Function/class declarations and other expressions are untouched.

Branch with fix + regression test (identifier default export resolves non-null callableId; calls 13/13, structure 28, dataflow 66, tsc/oxlint/oxfmt clean): https://github.com/CRYPTONIKAV/rea/tree/fix/3261-default-binding — happy for you to cherry-pick.

<!-- gh-comment-id:6163565222 --> @CRYPTONIKAV commented on GitHub (Oct 14, 2026): Traced this to `defaultDeclarationName` and have a minimal fix. PR creation from my fork is currently repository-blocked (REST 404), so leaving the patch here: **Root cause:** for `export default run`, `defaultDeclarationName` returns `null` (it only names Function/Class declarations), so the link records no `localName` and `resolveSemanticModuleCallables` never follows the lexical binding — `callableId` stays `null` and the return-shape projection bails. The `export { run as default }` control works because the specifier path records `localName` and the resolver follows it. **Fix** (`src/domain/javascript/javascriptSemanticProjection.ts`): a bare identifier keeps its name as `localName`; binding/initializer/callable resolution then runs the exact pre-existing path. Function/class declarations and other expressions are untouched. Branch with fix + regression test (identifier default export resolves non-null `callableId`; calls 13/13, structure 28, dataflow 66, `tsc`/`oxlint`/`oxfmt` clean): https://github.com/CRYPTONIKAV/rea/tree/fix/3261-default-binding — happy for you to cherry-pick.
Author
Owner

@CassieFox23 commented on GitHub (Oct 14, 2026):

Heads up: this looks fixed on main in dae1e5538. The change is in src/domain/javascript/javascriptSemanticReturns.ts. It is listed on tracker #3582. If it still happens on main, please say so here and I'll take it off the list.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6175032946 --> @CassieFox23 commented on GitHub (Oct 14, 2026): Heads up: this looks fixed on main in dae1e5538. The change is in `src/domain/javascript/javascriptSemanticReturns.ts`. It is listed on tracker #3582. If it still happens on main, please say so here and I'll take it off the list. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Resolved on main by dae1e5538.

Default-export expressions retain their lexical callable binding and captured return-shape semantics.

Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts and tests/acceptance/applications/exportedFunctionBindingsCli.test.ts.

<!-- gh-comment-id:6175062928 --> @morluto commented on GitHub (Oct 14, 2026): Resolved on main by dae1e5538. Default-export expressions retain their lexical callable binding and captured return-shape semantics. Rechecked on main b8510ee00 in the current focused run: 17 test files passed, 485 tests passed and one platform-dependent test skipped. Relevant regression coverage: src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts and tests/acceptance/applications/exportedFunctionBindingsCli.test.ts.
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#1167
No description provided.