[GH-ISSUE #5302] [Bug] Export return shapes give a var read before its initializer the initializer's value #3232

Closed
opened 2026-10-31 00:38:08 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @darshjoshi on GitHub (Oct 19, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5302

Searched open and closed issues and PRs for "hoist", "var initializer", and "read position". #2562 (fixed) covers initializers a branch or loop may skip. This case is straight-line code.

Area

Evidence, artifacts, or reconstruction

Observed behavior

compare-javascript-export-shapes reports a definite literal for a field that reads a var before its initializer runs. At runtime the field is undefined, because the declaration is hoisted but the assignment comes later.

Export body REA reports Node returns
return { kind: "k", a: v }; var v = 1; /a literal 1 a: undefined
var w = v; var v = 1; return { kind: "k", a: w }; /a literal 1 a: undefined

Controls behave correctly. var v = 1; return { kind: "k", a: v }; reports 1. const o = { kind: "k", a: v }; var v; return o; and if (false) { var v = 1; } return { kind: "k", a: v }; report no literal.

Reproduction

From a checkout at b4938aed built with npm run build:cached (Node 24.18.0):

WORK=$(mktemp -d)
for side in base app; do
  mkdir "$WORK/$side"
  echo '{"name":"probe","version":"1.0.0","main":"index.js"}' > "$WORK/$side/package.json"
done
echo 'exports.f = function () { return { kind: "k" }; };' > "$WORK/base/index.js"
echo 'exports.f = function () { return { kind: "k", a: v }; var v = 1; };' > "$WORK/app/index.js"
node scripts/rea.mjs analyze-javascript-application "$WORK/base" --format json > "$WORK/base.json" 2>/dev/null
node scripts/rea.mjs analyze-javascript-application "$WORK/app" --format json > "$WORK/app.json" 2>/dev/null
node -e '
const fs = require("fs"), w = process.argv[1];
const ev = (s) => JSON.parse(fs.readFileSync(`${w}/${s}.json`, "utf8"));
fs.writeFileSync(`${w}/in.json`, JSON.stringify({ left: ev("base"), right: ev("app"),
  left_module_path: "index.js", left_export_name: "f", right_module_path: "index.js", right_export_name: "f" }));' "$WORK"
node scripts/rea.mjs compare-javascript-export-shapes "$WORK/in.json" --format json \
  | node -e 'const n = JSON.parse(require("fs").readFileSync(0, "utf8")).normalized_result;
for (const c of n.changes) console.log(c.status, c.path, JSON.stringify(c.right));'
node -e 'console.log(require(process.argv[1]).f())' "$WORK/app/index.js"
rm -rf "$WORK"

Observed:

added /a {"availability":"literal","value":1}
{ kind: 'k', a: undefined }

Expected behavior

A read that runs before the var initializer should not get the initializer's value. It should be undefined or unknown, the same way reads before a later reassignment are already handled.

Impact and scope

Executed through the public CLI on macOS arm64, Node 24.18.0. The two shapes above reproduce every time. Each one produces a confident literal for a value the code never returns. I have not checked other consumers of the value lattice.

REA version

Source b4938aed (6.7.0)

Runtime and platform

Node 24.18.0, npm 11.16.0, macOS arm64

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Shown above. I also ran 81 other straight-line probes (spreads, getters, __proto__, compound assignment, aliasing, destructuring) the same way, comparing against Node. They produced no other incorrect literals.

Analysis or suspected cause

Source-derived. In evaluateExpression (src/domain/javascript/javascriptSemanticValues.ts), an identifier is resolved at its read position through semanticBindingAtRead only when the binding is mutable and has an assignment definition. A var whose only write is its declarator initializer skips that check. evaluateBinding then uses the initializer no matter where the read is. semanticBindingAtRead already drops initializers that do not come before the read.

I'm working on a fix and will open a PR.

Originally created by @darshjoshi on GitHub (Oct 19, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5302 Searched open and closed issues and PRs for "hoist", "var initializer", and "read position". #2562 (fixed) covers initializers a branch or loop may skip. This case is straight-line code. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `compare-javascript-export-shapes` reports a definite literal for a field that reads a `var` before its initializer runs. At runtime the field is `undefined`, because the declaration is hoisted but the assignment comes later. | Export body | REA reports | Node returns | | --- | --- | --- | | `return { kind: "k", a: v }; var v = 1;` | `/a` literal `1` | `a: undefined` | | `var w = v; var v = 1; return { kind: "k", a: w };` | `/a` literal `1` | `a: undefined` | Controls behave correctly. `var v = 1; return { kind: "k", a: v };` reports `1`. `const o = { kind: "k", a: v }; var v; return o;` and `if (false) { var v = 1; } return { kind: "k", a: v };` report no literal. ### Reproduction From a checkout at `b4938aed` built with `npm run build:cached` (Node 24.18.0): ```sh WORK=$(mktemp -d) for side in base app; do mkdir "$WORK/$side" echo '{"name":"probe","version":"1.0.0","main":"index.js"}' > "$WORK/$side/package.json" done echo 'exports.f = function () { return { kind: "k" }; };' > "$WORK/base/index.js" echo 'exports.f = function () { return { kind: "k", a: v }; var v = 1; };' > "$WORK/app/index.js" node scripts/rea.mjs analyze-javascript-application "$WORK/base" --format json > "$WORK/base.json" 2>/dev/null node scripts/rea.mjs analyze-javascript-application "$WORK/app" --format json > "$WORK/app.json" 2>/dev/null node -e ' const fs = require("fs"), w = process.argv[1]; const ev = (s) => JSON.parse(fs.readFileSync(`${w}/${s}.json`, "utf8")); fs.writeFileSync(`${w}/in.json`, JSON.stringify({ left: ev("base"), right: ev("app"), left_module_path: "index.js", left_export_name: "f", right_module_path: "index.js", right_export_name: "f" }));' "$WORK" node scripts/rea.mjs compare-javascript-export-shapes "$WORK/in.json" --format json \ | node -e 'const n = JSON.parse(require("fs").readFileSync(0, "utf8")).normalized_result; for (const c of n.changes) console.log(c.status, c.path, JSON.stringify(c.right));' node -e 'console.log(require(process.argv[1]).f())' "$WORK/app/index.js" rm -rf "$WORK" ``` Observed: ``` added /a {"availability":"literal","value":1} { kind: 'k', a: undefined } ``` ### Expected behavior A read that runs before the `var` initializer should not get the initializer's value. It should be `undefined` or unknown, the same way reads before a later reassignment are already handled. ### Impact and scope Executed through the public CLI on macOS arm64, Node 24.18.0. The two shapes above reproduce every time. Each one produces a confident literal for a value the code never returns. I have not checked other consumers of the value lattice. ### REA version Source `b4938aed` (6.7.0) ### Runtime and platform Node 24.18.0, npm 11.16.0, macOS arm64 ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Shown above. I also ran 81 other straight-line probes (spreads, getters, `__proto__`, compound assignment, aliasing, destructuring) the same way, comparing against Node. They produced no other incorrect literals. ### Analysis or suspected cause Source-derived. In `evaluateExpression` (`src/domain/javascript/javascriptSemanticValues.ts`), an identifier is resolved at its read position through `semanticBindingAtRead` only when the binding is mutable and has an `assignment` definition. A `var` whose only write is its declarator initializer skips that check. `evaluateBinding` then uses the initializer no matter where the read is. `semanticBindingAtRead` already drops initializers that do not come before the read. I'm working on a fix and will open a PR.
Author
Owner

@XinwangTao commented on GitHub (Oct 20, 2026):

Confirmed: this is the var hoisting case where the declaration is hoisted but the assignment is not, so a read before the initializer executes yields undefined at runtime. Reporting a confident literal there is a false-positive export shape that would silently mislead a comparison. Treating reads before their declarator as unknown/undefined (matching how reads before a later reassignment are handled) is the right fix, and the #5303 direction keeps the value lattice honest.

<!-- gh-comment-id:6281529392 --> @XinwangTao commented on GitHub (Oct 20, 2026): Confirmed: this is the var hoisting case where the declaration is hoisted but the assignment is not, so a read before the initializer executes yields undefined at runtime. Reporting a confident literal there is a false-positive export shape that would silently mislead a comparison. Treating reads before their declarator as unknown/undefined (matching how reads before a later reassignment are handled) is the right fix, and the #5303 direction keeps the value lattice honest.
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#3232
No description provided.