[GH-ISSUE #1428] [Bug] for...of over an array reports a stale literal after a member write #488

Closed
opened 2026-10-15 18:25:30 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @CassieFox23 on GitHub (Oct 10, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1428

TL;DR: A for...of loop over an array can change an object. REA misses that write. It then reports the old value as a sure fact. The real value is different.

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

The semantic pass reports literal 1 here. Real JS returns 2.

export function result() {
  const shared = { x: 1 };
  for (const t of [shared]) t.x = 2;
  return shared.x;
}

The loop variable t is shared. So t.x = 2 writes shared.x. REA does not link the two. It keeps the first value and calls it a literal.

The same happens with a named array: const arr = [shared]; for (const t of arr) t.x = 2;. That also gives literal 1.

A forEach call over the same array is handled well. It gives unknown, which is honest.

Reproduction

Pass the function above to analyzeJavaScriptSemantics. Then read the return site of result.

Source result()
Real JS (Node 22) 2
REA 6.3.0 literal 1
REA main at 499e2f54 literal 1
Same, with forEach unknown

Expected behavior

unknown, like the forEach form. Or literal 2 if the loop is followed exactly. Never a literal that real JS can't produce.

Impact and scope

This is a wrong fact, not a lost one. A caller would trust literal 1. It is not new: 6.3.0 already does it. Earlier heads of the #1401 fixes (#1390 at 56a40a43, #1414 at 8a95fecc) do not change it either. I have not tried their newest heads yet.

Interface

Other. I called the analysis function myself.

Earliest failure stage

Native analysis or reconstruction

Not run

  • Other loop forms: for...in, a plain for with an index, and destructuring in the loop head.
  • The MCP surface. I called the analysis function directly.
  • macOS or Windows.

Found while testing #1390 and #1414. Thanks to both authors for the loop cases that led here.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 22.23.1, 24.18.0, 26.11.1
  • REA: 6.3.0 (released package) and main at 499e2f54

🤖 Generated with Claude Code (Opus 5.5) 🦊

Originally created by @CassieFox23 on GitHub (Oct 10, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1428 **TL;DR:** A `for...of` loop over an array can change an object. REA misses that write. It then reports the old value as a sure fact. The real value is different. ### 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 The semantic pass reports `literal 1` here. Real JS returns 2. ```js export function result() { const shared = { x: 1 }; for (const t of [shared]) t.x = 2; return shared.x; } ``` The loop variable `t` is `shared`. So `t.x = 2` writes `shared.x`. REA does not link the two. It keeps the first value and calls it a literal. The same happens with a named array: `const arr = [shared]; for (const t of arr) t.x = 2;`. That also gives `literal 1`. A `forEach` call over the same array is handled well. It gives `unknown`, which is honest. ### Reproduction Pass the function above to `analyzeJavaScriptSemantics`. Then read the return site of `result`. | Source | `result()` | |---|---| | Real JS (Node 22) | `2` | | REA 6.3.0 | `literal 1` | | REA main at `499e2f54` | `literal 1` | | Same, with `forEach` | `unknown` | ### Expected behavior `unknown`, like the `forEach` form. Or `literal 2` if the loop is followed exactly. Never a literal that real JS can't produce. ### Impact and scope This is a wrong fact, not a lost one. A caller would trust `literal 1`. It is not new: 6.3.0 already does it. Earlier heads of the #1401 fixes (#1390 at `56a40a43`, #1414 at `8a95fecc`) do not change it either. I have not tried their newest heads yet. ### Interface Other. I called the analysis function myself. ### Earliest failure stage Native analysis or reconstruction ### Not run - Other loop forms: `for...in`, a plain `for` with an index, and destructuring in the loop head. - The MCP surface. I called the analysis function directly. - macOS or Windows. Found while testing #1390 and #1414. Thanks to both authors for the loop cases that led here. <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 22.23.1, 24.18.0, 26.11.1 - REA: 6.3.0 (released package) and main at `499e2f54` </details> 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

@akushonkamen commented on GitHub (Oct 10, 2026):

Confirmed on main (0b9815b). The gap is in collectSemanticMemberMutations:

  • src/domain/javascript/javascriptSemanticMemberMutations.ts:418 — in the stabilization loop, a fully enumerated known array with an iterationOnly reference hits if (knownArray && reference.iterationOnly) continue;, so the member write t.x = 2 is never projected onto element paths.
  • src/domain/javascript/javascriptSemanticMemberMutations.ts:485 (expandedIterationReferences) — the for-of expansion rewrites the selector to [name, ...projection], but when the selected element is an alias of another binding, the mutation is not propagated to that alias binding, so shared.x keeps its initial literal.

Working on a fix now: route the skipped case through the expansion (or conservatively fall back to the escape/unknown treatment the forEach path already uses), plus a regression test next to the existing for-of member-write cases. Never reporting a literal real JS cannot produce is the invariant.

Disclosure: prepared with AI assistance (ZCode CLI agent).

<!-- gh-comment-id:6093890872 --> @akushonkamen commented on GitHub (Oct 10, 2026): Confirmed on `main` (`0b9815b`). The gap is in `collectSemanticMemberMutations`: - `src/domain/javascript/javascriptSemanticMemberMutations.ts:418` — in the stabilization loop, a fully enumerated known array with an `iterationOnly` reference hits `if (knownArray && reference.iterationOnly) continue;`, so the member write `t.x = 2` is never projected onto element paths. - `src/domain/javascript/javascriptSemanticMemberMutations.ts:485` (`expandedIterationReferences`) — the for-of expansion rewrites the selector to `[name, ...projection]`, but when the selected element is an alias of another binding, the mutation is not propagated to that alias binding, so `shared.x` keeps its initial literal. Working on a fix now: route the skipped case through the expansion (or conservatively fall back to the `escape`/`unknown` treatment the `forEach` path already uses), plus a regression test next to the existing for-of member-write cases. Never reporting a literal real JS cannot produce is the invariant. Disclosure: prepared with AI assistance (ZCode CLI agent).
Author
Owner

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

TL;DR: Fixed on my box too. My repro now gives unknown, not literal 1. Both spellings, on Node 22, 24 and 26.

The full check is on https://github.com/morluto/rea/pull/1437#issuecomment-6094066075. It covers 38 loop cases. No new wrong answers turned up.

One related gap needs no loop at all: o.a = shared; o.a.x = 2. I filed that as #1442.

Thanks, N0zoM1z0, for the fix within the hour. And thanks, akushonkamen, for pinning down the exact lines.

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6094067058 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** Fixed on my box too. My repro now gives `unknown`, not `literal 1`. Both spellings, on Node 22, 24 and 26. The full check is on https://github.com/morluto/rea/pull/1437#issuecomment-6094066075. It covers 38 loop cases. No new wrong answers turned up. One related gap needs no loop at all: `o.a = shared; o.a.x = 2`. I filed that as #1442. Thanks, N0zoM1z0, for the fix within the hour. And thanks, akushonkamen, for pinning down the exact lines. 🤖 Generated with Claude Code (Opus 5.5) 🦊
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#488
No description provided.