[GH-ISSUE #3881] [Bug] Parameter captured before a later assignment is reported as the assigned literal #1501

Closed
opened 2026-10-16 21:06:15 +00:00 by bsakaguchi · 0 comments
Owner

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

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

For const saved = x; x = 2; return {type: "ok", value: saved};, rea analyze-javascript-application reports the returned /value as the literal 2 with complete return-shape coverage, although the function returns the caller's original x. rea compare-javascript-export-shapes against a function that really returns 2 then reports no changes, no unknowns, and complete-within-inputs coverage.

Both modules report /value as {"state": "literal", "value": 2}. The comparison returns zero added, removed, changed, and unknown entries, an empty change list, and complete-within-inputs coverage. Executing the fixtures with input 5 returns /value 5 for left and 2 for right. The error is already present in the producer's retained return shape; the comparison consumes it faithfully.

Reproduction

  1. Run the script below. It analyzes a left module that captures the parameter before a later assignment and a right module that returns the literal 2, prints both retained return fields, compares them, and executes both authored fixtures with input 5 as an independent control.
python3 - <<'PY'
import json, os, pathlib, shutil, subprocess, tempfile
work = pathlib.Path(tempfile.mkdtemp(prefix="rea-repro."))
(work / "home").mkdir()
env = dict(os.environ, HOME=str(work / "home"))
def cli(*args):
    result = subprocess.run(["rea", *args, "--format", "json"], env=env, capture_output=True, text=True, timeout=60, check=True)
    return json.loads(result.stdout)
sources = {
    "left": '''export function read(x) {
  const saved = x;
  x = 2;
  return {type: "ok", value: saved};
}
''',
    "right": '''export function read(x) {
  return {type: "ok", value: 2};
}
'''}
evidence = {}
for side, source in sources.items():
    (work / side).mkdir()
    (work / side / "main.mjs").write_text(source)
    evidence[side] = cli("analyze-javascript-application", str(work / side))
    graph = evidence[side]["normalized_result"]["graph"]
    shapes = [o["properties"] for n in graph["nodes"] for o in n["observations"] if o["properties"].get("semantic_role") == "export-return-shapes"]
    print(side, "fields:", json.dumps(shapes[0]["static_return_shapes"][0]["fields"]))
payload = dict(evidence, left_module_path="main.mjs", left_export_name="read", right_module_path="main.mjs", right_export_name="read")
(work / "input.json").write_text(json.dumps(payload))
comparison = cli("compare-javascript-export-shapes", str(work / "input.json"))["normalized_result"]
print("comparison:", json.dumps({"summary": comparison["summary"], "changes": comparison["changes"], "coverage": comparison["coverage"]["status"]}))
probe = subprocess.run(["node", "--input-type=module", "-e",
    'import {pathToFileURL} from "node:url"; const left = await import(pathToFileURL(process.argv[1])); '
    'const right = await import(pathToFileURL(process.argv[2])); '
    'console.log(JSON.stringify([left.read(5),right.read(5)]));',
    str(work / "left" / "main.mjs"), str(work / "right" / "main.mjs")], env=env, capture_output=True, text=True, check=True, timeout=10)
print("fixture runtime:", probe.stdout.strip())
shutil.rmtree(work)
PY

Expected behavior

The left module's /value stays caller-dependent (unknown), because saved captured the incoming parameter before x = 2. The comparison with the constant-2 right module then reports /value as unknown rather than complete equality. docs/javascript-application-workflows.md describes direct return expressions as evaluated through an execution-free value lattice and keeps unresolved values unknown when the projections differ.

Impact and scope

Functions that capture a parameter before its sole later assignment publish a false exact return value, and export-shape comparison then hides a real return difference; the effect is bounded semantic-evidence incorrectness.

Workaround: unverified. No public option that keeps the captured value unknown was established; a reader can only inspect the source manually.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

Local JavaScript ES module directory

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/javascript-application-workflows.md:207-210 "Export return-shape comparison" (direct return expressions use an execution-free value lattice) and docs/javascript-application-workflows.md:258-260 (unresolved values stay unknown when projections differ)
  • Failure source: observed analyze-javascript-application export return-shape fields and compare-javascript-export-shapes output for the fixtures above, with the fixtures' own runtime results as an independent control
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete
left fields: [{"path": "/type", "presence": "present", "state": "literal", "value": "ok", "reason": null}, {"path": "/value", "presence": "present", "state": "literal", "value": 2, "reason": null}]
right fields: [{"path": "/type", "presence": "present", "state": "literal", "value": "ok", "reason": null}, {"path": "/value", "presence": "present", "state": "literal", "value": 2, "reason": null}]
comparison: {"summary": {"added": 0, "removed": 0, "changed": 0, "unknown": 0}, "changes": [], "coverage": "complete-within-inputs"}
fixture runtime: [{"type":"ok","value":5},{"type":"ok","value":2}]

Source-derived reasoning: src/domain/javascript/javascriptSemanticReturns.ts:70 evaluates return expressions against shared semantic state. Identifier evaluation at src/domain/javascript/javascriptSemanticValues.ts:478 calls the whole-binding evaluator, and evaluateBinding (lines 355-379) uses the parameter's sole assignment initializer without requiring it to precede the read. The capture fallback at lines 303-315 reverts to live evaluation, so it cannot recover the unknown incoming value. src/domain/javascript/javascriptExportReturnShapeProjection.ts:89 flattens the result into the export observation. A separate direct domain probe of export function read(x) { if (x) return x; x = 2; return x; } also reported both return sites as literal 2, while direct domain controls kept an unchanged parameter return unknown and an assignment-before-read return equal to 2.

Typed outcome or recovery expected

The captured value should stay caller-dependent (unknown) in the export return shape, and export-shape comparison against a constant should report the field as unknown rather than complete equality.

Restoration check:

For the left fixture, /value currently prints "state": "literal", "value": 2 and the comparison prints "unknown": 0 with complete-within-inputs; after a fix /value is unknown (caller-dependent) and the comparison reports /value as unknown or partial rather than equal. Control: the right fixture continues to report /value as literal 2, and a function that assigns x = 2 before return {value: x} continues to report literal 2.

Analysis or suspected cause

Source-derived: src/domain/javascript/javascriptSemanticValues.ts:355-379 (evaluateBinding) uses the parameter's sole assignment initializer without requiring it to precede the read; the capture fallback at lines 303-315 reverts to live evaluation; src/domain/javascript/javascriptExportReturnShapeProjection.ts:89 flattens the result into the export observation.

Additional context

Related closed #2646 concerned conditional logical-assignment value evaluation; this fixture uses an unconditional later plain assignment.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3881 ### 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 For `const saved = x; x = 2; return {type: "ok", value: saved};`, `rea analyze-javascript-application` reports the returned `/value` as the literal `2` with complete return-shape coverage, although the function returns the caller's original `x`. `rea compare-javascript-export-shapes` against a function that really returns `2` then reports no changes, no unknowns, and `complete-within-inputs` coverage. Both modules report `/value` as `{"state": "literal", "value": 2}`. The comparison returns zero added, removed, changed, and unknown entries, an empty change list, and `complete-within-inputs` coverage. Executing the fixtures with input `5` returns `/value` `5` for `left` and `2` for `right`. The error is already present in the producer's retained return shape; the comparison consumes it faithfully. ### Reproduction 1. Run the script below. It analyzes a `left` module that captures the parameter before a later assignment and a `right` module that returns the literal `2`, prints both retained return fields, compares them, and executes both authored fixtures with input `5` as an independent control. ```sh python3 - <<'PY' import json, os, pathlib, shutil, subprocess, tempfile work = pathlib.Path(tempfile.mkdtemp(prefix="rea-repro.")) (work / "home").mkdir() env = dict(os.environ, HOME=str(work / "home")) def cli(*args): result = subprocess.run(["rea", *args, "--format", "json"], env=env, capture_output=True, text=True, timeout=60, check=True) return json.loads(result.stdout) sources = { "left": '''export function read(x) { const saved = x; x = 2; return {type: "ok", value: saved}; } ''', "right": '''export function read(x) { return {type: "ok", value: 2}; } '''} evidence = {} for side, source in sources.items(): (work / side).mkdir() (work / side / "main.mjs").write_text(source) evidence[side] = cli("analyze-javascript-application", str(work / side)) graph = evidence[side]["normalized_result"]["graph"] shapes = [o["properties"] for n in graph["nodes"] for o in n["observations"] if o["properties"].get("semantic_role") == "export-return-shapes"] print(side, "fields:", json.dumps(shapes[0]["static_return_shapes"][0]["fields"])) payload = dict(evidence, left_module_path="main.mjs", left_export_name="read", right_module_path="main.mjs", right_export_name="read") (work / "input.json").write_text(json.dumps(payload)) comparison = cli("compare-javascript-export-shapes", str(work / "input.json"))["normalized_result"] print("comparison:", json.dumps({"summary": comparison["summary"], "changes": comparison["changes"], "coverage": comparison["coverage"]["status"]})) probe = subprocess.run(["node", "--input-type=module", "-e", 'import {pathToFileURL} from "node:url"; const left = await import(pathToFileURL(process.argv[1])); ' 'const right = await import(pathToFileURL(process.argv[2])); ' 'console.log(JSON.stringify([left.read(5),right.read(5)]));', str(work / "left" / "main.mjs"), str(work / "right" / "main.mjs")], env=env, capture_output=True, text=True, check=True, timeout=10) print("fixture runtime:", probe.stdout.strip()) shutil.rmtree(work) PY ``` ### Expected behavior The `left` module's `/value` stays caller-dependent (unknown), because `saved` captured the incoming parameter before `x = 2`. The comparison with the constant-2 `right` module then reports `/value` as unknown rather than complete equality. `docs/javascript-application-workflows.md` describes direct return expressions as evaluated through an execution-free value lattice and keeps unresolved values `unknown` when the projections differ. ### Impact and scope Functions that capture a parameter before its sole later assignment publish a false exact return value, and export-shape comparison then hides a real return difference; the effect is bounded semantic-evidence incorrectness. Workaround: unverified. No public option that keeps the captured value unknown was established; a reader can only inspect the source manually. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context Local JavaScript ES module directory ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: docs/javascript-application-workflows.md:207-210 "Export return-shape comparison" (direct return expressions use an execution-free value lattice) and docs/javascript-application-workflows.md:258-260 (unresolved values stay `unknown` when projections differ) - Failure source: observed `analyze-javascript-application` export return-shape fields and `compare-javascript-export-shapes` output for the fixtures above, with the fixtures' own runtime results as an independent control - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ```text left fields: [{"path": "/type", "presence": "present", "state": "literal", "value": "ok", "reason": null}, {"path": "/value", "presence": "present", "state": "literal", "value": 2, "reason": null}] right fields: [{"path": "/type", "presence": "present", "state": "literal", "value": "ok", "reason": null}, {"path": "/value", "presence": "present", "state": "literal", "value": 2, "reason": null}] comparison: {"summary": {"added": 0, "removed": 0, "changed": 0, "unknown": 0}, "changes": [], "coverage": "complete-within-inputs"} fixture runtime: [{"type":"ok","value":5},{"type":"ok","value":2}] ``` Source-derived reasoning: `src/domain/javascript/javascriptSemanticReturns.ts:70` evaluates return expressions against shared semantic state. Identifier evaluation at `src/domain/javascript/javascriptSemanticValues.ts:478` calls the whole-binding evaluator, and `evaluateBinding` (lines 355-379) uses the parameter's sole assignment initializer without requiring it to precede the read. The capture fallback at lines 303-315 reverts to live evaluation, so it cannot recover the unknown incoming value. `src/domain/javascript/javascriptExportReturnShapeProjection.ts:89` flattens the result into the export observation. A separate direct domain probe of `export function read(x) { if (x) return x; x = 2; return x; }` also reported both return sites as literal `2`, while direct domain controls kept an unchanged parameter return unknown and an assignment-before-read return equal to `2`. ### Typed outcome or recovery expected The captured value should stay caller-dependent (unknown) in the export return shape, and export-shape comparison against a constant should report the field as unknown rather than complete equality. Restoration check: For the `left` fixture, `/value` currently prints `"state": "literal", "value": 2` and the comparison prints `"unknown": 0` with `complete-within-inputs`; after a fix `/value` is unknown (caller-dependent) and the comparison reports `/value` as unknown or partial rather than equal. Control: the `right` fixture continues to report `/value` as literal `2`, and a function that assigns `x = 2` before `return {value: x}` continues to report literal `2`. ### Analysis or suspected cause Source-derived: src/domain/javascript/javascriptSemanticValues.ts:355-379 (evaluateBinding) uses the parameter's sole assignment initializer without requiring it to precede the read; the capture fallback at lines 303-315 reverts to live evaluation; src/domain/javascript/javascriptExportReturnShapeProjection.ts:89 flattens the result into the export observation. ### Additional context Related closed #2646 concerned conditional logical-assignment value evaluation; this fixture uses an unconditional later plain assignment. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
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#1501
No description provided.