[GH-ISSUE #1449] [Bug] Writing through a function's returned object reports a stale literal #491

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

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

TL;DR: A function returns an object. You write to it through the call. REA misses that write. It reports the old value as a sure fact. No slot or loop is needed.

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 return shape says literal 1 here. Real JS returns 2.

export function result() {
  const shared = { x: 1 };
  const get = () => shared;
  get().x = 2;
  return shared.x;
}

get() returns shared itself. So get().x = 2 writes shared.x. REA does not link the two. Coverage still says complete.

Reproduction

Save each form as app.js in its own folder. Run rea analyze-javascript-application <dir> --artifact-format directory --json. Then read static_return_shapes on result.

Form Real JS 6.3.0 main
get().x = 2 2 literal 1 literal 1
const t = get(); t.x = 2 2 literal 1 literal 1
function get() { return shared; }, then o.a = get(); o.a.x = 2 2 literal 1 literal 1
o.a = (() => shared)(); o.a.x = 2 2 literal 1 literal 1
const box = { get() { return shared; } }; o.a = box.get(); o.a.x = 2 2 literal 1 literal 1
o.a = Object(shared); o.a.x = 2 2 unknown unknown

Main here means #1445 at c56a3866, which merged as e09cfe9b. It has the same file tree.

Expected behavior

unknown, like the Object(shared) form. Or literal 2 if the write is followed. 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 does it too. #1445 fixed slot stores from #1442. These forms got past it. The first two have no slot at all. So the gap seems to be a reference that a call returns.

Interface

CLI and MCP. They gave the same answer.

Earliest failure stage

Native analysis or reconstruction

Not run

Edit: results are in the follow-up. Property thunks, async calls and generators all give the same stale literal 1. Still not run: macOS and Windows.

  • Calls that return a property, like () => o.inner.
  • Async functions and generators that hand back the object.
  • Calls into another module. Ran. Real JS gives 2 only when the writing file loads first.
  • macOS or Windows.

Found while testing #1445. Thanks to N0zoM1z0 for that fast fix.

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), base 33b9b31a, and #1445 at c56a3866

🤖 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/1449 **TL;DR:** A function returns an object. You write to it through the call. REA misses that write. It reports the old value as a sure fact. No slot or loop is needed. ### 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 return shape says `literal 1` here. Real JS returns 2. ```js export function result() { const shared = { x: 1 }; const get = () => shared; get().x = 2; return shared.x; } ``` `get()` returns `shared` itself. So `get().x = 2` writes `shared.x`. REA does not link the two. Coverage still says `complete`. ### Reproduction Save each form as `app.js` in its own folder. Run `rea analyze-javascript-application <dir> --artifact-format directory --json`. Then read `static_return_shapes` on `result`. | Form | Real JS | 6.3.0 | main | |---|---|---|---| | `get().x = 2` | 2 | `literal 1` | `literal 1` | | `const t = get(); t.x = 2` | 2 | `literal 1` | `literal 1` | | `function get() { return shared; }`, then `o.a = get(); o.a.x = 2` | 2 | `literal 1` | `literal 1` | | `o.a = (() => shared)(); o.a.x = 2` | 2 | `literal 1` | `literal 1` | | `const box = { get() { return shared; } }; o.a = box.get(); o.a.x = 2` | 2 | `literal 1` | `literal 1` | | `o.a = Object(shared); o.a.x = 2` | 2 | `unknown` | `unknown` | Main here means #1445 at `c56a3866`, which merged as `e09cfe9b`. It has the same file tree. ### Expected behavior `unknown`, like the `Object(shared)` form. Or `literal 2` if the write is followed. 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 does it too. #1445 fixed slot stores from #1442. These forms got past it. The first two have no slot at all. So the gap seems to be a reference that a call returns. ### Interface CLI and MCP. They gave the same answer. ### Earliest failure stage Native analysis or reconstruction ### Not run Edit: results are in [the follow-up](https://github.com/morluto/rea/issues/1449#issuecomment-6094933620). Property thunks, async calls and generators all give the same stale `literal 1`. Still not run: macOS and Windows. - ~~Calls that return a property, like `() => o.inner`.~~ - ~~Async functions and generators that hand back the object.~~ - ~~Calls into another module.~~ Ran. Real JS gives 2 only when the writing file loads first. - macOS or Windows. Found while testing #1445. Thanks to N0zoM1z0 for that fast fix. <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), base `33b9b31a`, and #1445 at `c56a3866` </details> 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

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

Taking this one. Root cause confirmed locally: referencedValues() in src/domain/javascript/javascriptSemanticMemberMutations.ts (return [] fallback, ~line 571 on current main) has no CallExpression branch, so writes like get().x = 2 never attribute to the underlying binding and mutatedPaths stays empty — stale literal + false coverage-complete. Preparing a fix with regression tests; PR to follow shortly.

Disclosure: this fix is being prepared with AI assistance (Claude/ZCode agent), reviewed and tested by a human before submission.

<!-- gh-comment-id:6094644118 --> @akushonkamen commented on GitHub (Oct 10, 2026): Taking this one. Root cause confirmed locally: `referencedValues()` in `src/domain/javascript/javascriptSemanticMemberMutations.ts` (`return []` fallback, ~line 571 on current main) has no CallExpression branch, so writes like `get().x = 2` never attribute to the underlying binding and `mutatedPaths` stays empty — stale literal + false coverage-complete. Preparing a fix with regression tests; PR to follow shortly. Disclosure: this fix is being prepared with AI assistance (Claude/ZCode agent), reviewed and tested by a human before submission.
Author
Owner

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

PR: #1468 — 8 red→green regression cases plus no-over-invalidation guards; fast projects 3634 passed, 0 failed.

<!-- gh-comment-id:6094763163 --> @akushonkamen commented on GitHub (Oct 10, 2026): PR: #1468 — 8 red→green regression cases plus no-over-invalidation guards; fast projects 3634 passed, 0 failed.
Author
Owner

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

TL;DR: Property thunks, async calls and generators give the same stale literal 1. PR #1468 fixes the property thunks. Methods, async and generators stay wrong there.

All rows below return 2 in real JS. Coverage says complete on every one. For the two b.js rows, real JS loads b.js first.

Form Real JS 6.3.0 main #1468
() => o.inner, () => o.a.b, () => arr[0] 2 literal 1 literal 1 unknown
get().x = 2 at module top level 2 literal 1 literal 1 unknown
(await get()).x = 2 2 literal 1 literal 1 literal 1
await get().then(t => { t.x = 2 }) 2 literal 1 literal 1 literal 1
g().next().value.x = 2 2 literal 1 literal 1 literal 1
[...g()][0].x = 2, for (const t of g()) 2 literal 1 literal 1 literal 1
box.get().x, new B().get().x, () => get(), () => (f ? shared : other), .bind 2 literal 1 literal 1 literal 1
b.js calls get().x = 2, a.js returns shared.x 2 literal 1 literal 1 literal 1
b.js writes shared.x = 2, a.js returns it 2 literal 1 literal 1 literal 1

The last row has no call at all. If only a.js runs, real JS gives 1, so literal 1 depends on load order. Still, complete coverage seems strong for an exported object that any importer can change. It may be worth a look on its own.

() => this.x in a method gives unknown everywhere. That is honest.

Controls: 19 forms where 1 is right. They include a fresh object and a copy. Others return a primitive, write another field or never write. On 6.3.0 and main, 17 keep literal 1. Two give unknown. Both read a value from an import. #1468 moves one more to unknown. That is a let get later pointed at another object. It is safe, just less precise.

Node 22, 24 and 26 agree on every row. CLI and MCP agree too.

Still not run

Edit: async generators ran on main and #1471, in the #1471 results. Main gives the stale literal 1, and #1471 gives unknown. #1471 fixes most rows here, but getters stay wrong. Still not run: CommonJS, the importer rows, macOS and Windows.

  • CommonJS. REA gave no return shape for any CJS export. So these never reached the bug.
  • ESM where the importer returns shared.x. An import read is always unknown, even with no write. So these rows can't show the bug.
  • Async generators.
  • macOS and Windows.

Thanks, akushonkamen, for taking this so fast.

Full matrix (64 cases)

Each case is export function result() in app.js, unless files are named. In the cross-module rows, real JS loaded b.js before calling result(). With a.js alone, real JS gives 1. The CLI on Node 22 is shown. Node 24, Node 26 and MCP gave the same.

Code drops the shared const shared = { x: 1 } line and the final return shared.x.

Form Real JS 6.3.0 main #1468 #1468 verdict
const get = () => shared; get().x = 2; 2 literal 1 literal 1 unknown honest
const get = () => shared; const t = get(); t.x = 2; 2 literal 1 literal 1 unknown honest
function get() { return shared; } const o = {}; o.a = get(); o.a.x = 2; 2 literal 1 literal 1 unknown honest
const o = {}; o.a = (() => shared)(); o.a.x = 2; 2 literal 1 literal 1 unknown honest
const box = { get() { return shared; } }; const o = {}; o.a = box.get(); o.a.x = 2; 2 literal 1 literal 1 literal 1 WRONG
const o = {}; o.a = Object(shared); o.a.x = 2; 2 unknown unknown unknown honest
function get() { return shared; } get().x = 2; 2 literal 1 literal 1 unknown honest
const get = function () { return shared; }; get().x = 2; 2 literal 1 literal 1 unknown honest
(() => shared)().x = 2; 2 literal 1 literal 1 unknown honest
const box = { get() { return shared; } }; box.get().x = 2; 2 literal 1 literal 1 literal 1 WRONG
class B { get() { return shared; } } new B().get().x = 2; 2 literal 1 literal 1 literal 1 WRONG
const id = (v) => v; id(shared).x = 2; 2 unknown unknown unknown honest
const o = { inner: { y: 1 } }; const pick = (p) => p.inner; pick(o).y = 2; return o.inner.y; 2 unknown unknown unknown honest
const get = () => shared; const get2 = () => get(); get2().x = 2; 2 literal 1 literal 1 literal 1 WRONG
const other = { x: 1 }; const f = true; const get = () => (f ? shared : other); get().x = 2; 2 literal 1 literal 1 literal 1 WRONG
const other = { x: 1 }; const get = (f) => { if (f) return shared; return other; }; get(true).x = 2; 2 literal 1 literal 1 unknown honest
const other = { x: 1 }; let get = () => other; get = () => shared; get().x = 2; 2 literal 1 literal 1 unknown honest
const get = () => shared; get().x += 1; 2 literal 1 literal 1 unknown honest
const get = (() => shared).bind(null); get().x = 2; 2 literal 1 literal 1 literal 1 WRONG
const get = () => shared; Object.assign(get(), { x: 2 }); 2 literal 1 literal 1 unknown honest
const o = { inner: { y: 1 } }; const get = () => o.inner; get().y = 2; return o.inner.y; 2 literal 1 literal 1 unknown honest
const shared = { y: 1 }; const o = { inner: shared }; const get = () => o.inner; get().y = 2; return shared.y; 2 literal 1 literal 1 unknown honest
const o = { a: { b: { z: 1 } } }; const get = () => o.a.b; get().z = 2; return o.a.b.z; 2 literal 1 literal 1 unknown honest
const arr = [{ x: 1 }]; const get = () => arr[0]; get().x = 2; return arr[0].x; 2 literal 1 literal 1 unknown honest
const o = { x: { y: 1 }, get() { return this.x; } }; o.get().y = 2; return o.x.y; 2 unknown unknown unknown honest
async result: async function get() { return shared; } (await get()).x = 2; 2 literal 1 literal 1 literal 1 WRONG
async result: async function get() { return shared; } await get().then((t) => { t.x = 2; }); 2 literal 1 literal 1 literal 1 WRONG
function* g() { yield shared; } g().next().value.x = 2; 2 literal 1 literal 1 literal 1 WRONG
function* g() { yield shared; } [...g()][0].x = 2; 2 literal 1 literal 1 literal 1 WRONG
function* g() { yield shared; } for (const t of g()) t.x = 2; 2 literal 1 literal 1 literal 1 WRONG
a.js exports shared, get = () => shared / b.js result: get().x = 2; return shared.x 2 unknown unknown unknown honest
a.js exports shared, get / b.js top level: get().x = 2; result returns shared.x 2 unknown unknown unknown honest
a.js exports shared, get, result returns shared.x / b.js top level: get().x = 2 2 literal 1 literal 1 literal 1 WRONG
a.js exports shared, get / mid.js: export { shared, get } from './a.js' / b.js imports mid.js, result: get().x = 2; return shared.x 2 unknown unknown unknown honest
a.js exports shared, get / mid.js: export * from './a.js' / b.js imports mid.js, result: get().x = 2; return shared.x 2 unknown unknown unknown honest
CJS: a.js module.exports = { shared, get } / b.js result: get().x = 2; return shared.x 2 no shape no shape no shape not reached
CJS: a.js exports shared, get, result / b.js: require('./a.js').get().x = 2 2 no shape no shape no shape not reached
CJS: mid.js: module.exports = require('./a.js') / b.js requires mid.js, result: get().x = 2; return shared.x 2 no shape no shape no shape not reached
module-level shared, get; result returns shared.x; top level: get().x = 2 2 literal 1 literal 1 unknown honest
shared.x = 2; 2 unknown unknown unknown honest
const o = { inner: { y: 1 } }; o.inner.y = 2; return o.inner.y; 2 unknown unknown unknown honest
a.js exports shared / b.js result: shared.x = 2; return shared.x 2 unknown unknown unknown honest
CJS: a.js exports shared / b.js result: shared.x = 2; return shared.x 2 no shape no shape no shape not reached
a.js exports shared, result returns shared.x / b.js top level: shared.x = 2 2 literal 1 literal 1 literal 1 WRONG
module-level shared; result returns shared.x; top level: shared.x = 2 2 unknown unknown unknown honest
const get = () => ({ x: 1 }); get().x = 2; 1 literal 1 literal 1 literal 1 correct
const get = () => ({ ...shared }); get().x = 2; 1 literal 1 literal 1 literal 1 correct
const get = () => shared.x; let v = get(); v = 2; 1 literal 1 literal 1 literal 1 correct
const get = () => shared; const t = get(); const y = t.x; 1 literal 1 literal 1 literal 1 correct
const get = () => shared; get().y = 2; 1 literal 1 literal 1 literal 1 correct
const other = { x: 1 }; const get = () => other; get().x = 2; 1 literal 1 literal 1 literal 1 correct
const o = { inner: { y: 1 }, other: { y: 1 } }; const get = () => o.other; get().y = 2; return o.inner.y; 1 literal 1 literal 1 literal 1 correct
function* g() { yield { x: 1 }; } g().next().value.x = 2; 1 literal 1 literal 1 literal 1 correct
async result: async function get() { return { x: 1 }; } (await get()).x = 2; 1 literal 1 literal 1 literal 1 correct
async function get() { return shared; } get().then((t) => { t.x = 2; }); 1 literal 1 literal 1 literal 1 correct
a.js exports shared, make = () => ({ x: 1 }) / b.js result: make().x = 2; return shared.x 1 unknown unknown unknown lost precision
a.js exports shared / b.js result: return shared.x (no write anywhere) 1 unknown unknown unknown lost precision
a.js exports shared, get, result returns shared.x / b.js: export const y = get().x 1 literal 1 literal 1 literal 1 correct
const shared = { x: 1, inner: { y: 1 } }; const get = () => shared.inner; get().y = 2; 1 literal 1 literal 1 literal 1 correct
const make = (v) => ({ x: v }); make(1).x = 2; 1 literal 1 literal 1 literal 1 correct
(() => ({ x: 1 }))().x = 2; 1 literal 1 literal 1 literal 1 correct
const get = async () => shared; get().x = 2; 1 literal 1 literal 1 literal 1 correct
function* g() { yield shared; } g().x = 2; 1 literal 1 literal 1 literal 1 correct
const other = { x: 1 }; let get = () => shared; get = () => other; get().x = 2; 1 literal 1 literal 1 unknown lost precision
Commands
  • Real JS: each case runs in its own node child. ESM uses import(), CJS uses require.
  • CLI: rea analyze-javascript-application <dir> --artifact-format directory --json.
  • MCP: one stdio server per tree and Node, calling analyze_javascript_application.
  • I read the result shape from the module that defines it.
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), main 4e3d3354, #1468 head 9c559f3e

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6094933620 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** Property thunks, async calls and generators give the same stale `literal 1`. PR #1468 fixes the property thunks. Methods, async and generators stay wrong there. All rows below return 2 in real JS. Coverage says `complete` on every one. For the two `b.js` rows, real JS loads `b.js` first. | Form | Real JS | 6.3.0 | main | #1468 | |---|---|---|---|---| | `() => o.inner`, `() => o.a.b`, `() => arr[0]` | 2 | `literal 1` | `literal 1` | `unknown` | | `get().x = 2` at module top level | 2 | `literal 1` | `literal 1` | `unknown` | | `(await get()).x = 2` | 2 | `literal 1` | `literal 1` | `literal 1` | | `await get().then(t => { t.x = 2 })` | 2 | `literal 1` | `literal 1` | `literal 1` | | `g().next().value.x = 2` | 2 | `literal 1` | `literal 1` | `literal 1` | | `[...g()][0].x = 2`, `for (const t of g())` | 2 | `literal 1` | `literal 1` | `literal 1` | | `box.get().x`, `new B().get().x`, `() => get()`, `() => (f ? shared : other)`, `.bind` | 2 | `literal 1` | `literal 1` | `literal 1` | | `b.js` calls `get().x = 2`, `a.js` returns `shared.x` | 2 | `literal 1` | `literal 1` | `literal 1` | | `b.js` writes `shared.x = 2`, `a.js` returns it | 2 | `literal 1` | `literal 1` | `literal 1` | The last row has no call at all. If only `a.js` runs, real JS gives 1, so `literal 1` depends on load order. Still, `complete` coverage seems strong for an exported object that any importer can change. It may be worth a look on its own. `() => this.x` in a method gives `unknown` everywhere. That is honest. **Controls:** 19 forms where 1 is right. They include a fresh object and a copy. Others return a primitive, write another field or never write. On 6.3.0 and main, 17 keep `literal 1`. Two give `unknown`. Both read a value from an import. #1468 moves one more to `unknown`. That is a `let get` later pointed at another object. It is safe, just less precise. Node 22, 24 and 26 agree on every row. CLI and MCP agree too. ### Still not run Edit: async generators ran on main and #1471, in [the #1471 results](https://github.com/morluto/rea/pull/1471#issuecomment-6095203282). Main gives the stale `literal 1`, and #1471 gives `unknown`. #1471 fixes most rows here, but getters stay wrong. Still not run: CommonJS, the importer rows, macOS and Windows. - CommonJS. REA gave no return shape for any CJS export. So these never reached the bug. - ESM where the importer returns `shared.x`. An import read is always `unknown`, even with no write. So these rows can't show the bug. - ~~Async generators.~~ - macOS and Windows. Thanks, akushonkamen, for taking this so fast. <details> <summary>Full matrix (64 cases)</summary> Each case is `export function result()` in `app.js`, unless files are named. In the cross-module rows, real JS loaded `b.js` before calling `result()`. With `a.js` alone, real JS gives 1. The CLI on Node 22 is shown. Node 24, Node 26 and MCP gave the same. Code drops the shared `const shared = { x: 1 }` line and the final `return shared.x`. | Form | Real JS | 6.3.0 | main | #1468 | #1468 verdict | |---|---|---|---|---|---| | `const get = () => shared; get().x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const get = () => shared; const t = get(); t.x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `function get() { return shared; } const o = {}; o.a = get(); o.a.x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const o = {}; o.a = (() => shared)(); o.a.x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const box = { get() { return shared; } }; const o = {}; o.a = box.get(); o.a.x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `const o = {}; o.a = Object(shared); o.a.x = 2;` | 2 | `unknown` | `unknown` | `unknown` | honest | | `function get() { return shared; } get().x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const get = function () { return shared; }; get().x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `(() => shared)().x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const box = { get() { return shared; } }; box.get().x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `class B { get() { return shared; } } new B().get().x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `const id = (v) => v; id(shared).x = 2;` | 2 | `unknown` | `unknown` | `unknown` | honest | | `const o = { inner: { y: 1 } }; const pick = (p) => p.inner; pick(o).y = 2; return o.inner.y;` | 2 | `unknown` | `unknown` | `unknown` | honest | | `const get = () => shared; const get2 = () => get(); get2().x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `const other = { x: 1 }; const f = true; const get = () => (f ? shared : other); get().x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `const other = { x: 1 }; const get = (f) => { if (f) return shared; return other; }; get(true).x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const other = { x: 1 }; let get = () => other; get = () => shared; get().x = 2;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const get = () => shared; get().x += 1;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const get = (() => shared).bind(null); get().x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `const get = () => shared; Object.assign(get(), { x: 2 });` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const o = { inner: { y: 1 } }; const get = () => o.inner; get().y = 2; return o.inner.y;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const shared = { y: 1 }; const o = { inner: shared }; const get = () => o.inner; get().y = 2; return shared.y;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const o = { a: { b: { z: 1 } } }; const get = () => o.a.b; get().z = 2; return o.a.b.z;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const arr = [{ x: 1 }]; const get = () => arr[0]; get().x = 2; return arr[0].x;` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `const o = { x: { y: 1 }, get() { return this.x; } }; o.get().y = 2; return o.x.y;` | 2 | `unknown` | `unknown` | `unknown` | honest | | `async result: async function get() { return shared; } (await get()).x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `async result: async function get() { return shared; } await get().then((t) => { t.x = 2; });` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `function* g() { yield shared; } g().next().value.x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `function* g() { yield shared; } [...g()][0].x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `function* g() { yield shared; } for (const t of g()) t.x = 2;` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `a.js exports shared, get = () => shared / b.js result: get().x = 2; return shared.x` | 2 | `unknown` | `unknown` | `unknown` | honest | | `a.js exports shared, get / b.js top level: get().x = 2; result returns shared.x` | 2 | `unknown` | `unknown` | `unknown` | honest | | `a.js exports shared, get, result returns shared.x / b.js top level: get().x = 2` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `a.js exports shared, get / mid.js: export { shared, get } from './a.js' / b.js imports mid.js, result: get().x = 2; return shared.x` | 2 | `unknown` | `unknown` | `unknown` | honest | | `a.js exports shared, get / mid.js: export * from './a.js' / b.js imports mid.js, result: get().x = 2; return shared.x` | 2 | `unknown` | `unknown` | `unknown` | honest | | `CJS: a.js module.exports = { shared, get } / b.js result: get().x = 2; return shared.x` | 2 | no shape | no shape | no shape | not reached | | `CJS: a.js exports shared, get, result / b.js: require('./a.js').get().x = 2` | 2 | no shape | no shape | no shape | not reached | | `CJS: mid.js: module.exports = require('./a.js') / b.js requires mid.js, result: get().x = 2; return shared.x` | 2 | no shape | no shape | no shape | not reached | | `module-level shared, get; result returns shared.x; top level: get().x = 2` | 2 | `literal 1` | `literal 1` | `unknown` | honest | | `shared.x = 2;` | 2 | `unknown` | `unknown` | `unknown` | honest | | `const o = { inner: { y: 1 } }; o.inner.y = 2; return o.inner.y;` | 2 | `unknown` | `unknown` | `unknown` | honest | | `a.js exports shared / b.js result: shared.x = 2; return shared.x` | 2 | `unknown` | `unknown` | `unknown` | honest | | `CJS: a.js exports shared / b.js result: shared.x = 2; return shared.x` | 2 | no shape | no shape | no shape | not reached | | `a.js exports shared, result returns shared.x / b.js top level: shared.x = 2` | 2 | `literal 1` | `literal 1` | `literal 1` | WRONG | | `module-level shared; result returns shared.x; top level: shared.x = 2` | 2 | `unknown` | `unknown` | `unknown` | honest | | `const get = () => ({ x: 1 }); get().x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const get = () => ({ ...shared }); get().x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const get = () => shared.x; let v = get(); v = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const get = () => shared; const t = get(); const y = t.x;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const get = () => shared; get().y = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const other = { x: 1 }; const get = () => other; get().x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const o = { inner: { y: 1 }, other: { y: 1 } }; const get = () => o.other; get().y = 2; return o.inner.y;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `function* g() { yield { x: 1 }; } g().next().value.x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `async result: async function get() { return { x: 1 }; } (await get()).x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `async function get() { return shared; } get().then((t) => { t.x = 2; });` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `a.js exports shared, make = () => ({ x: 1 }) / b.js result: make().x = 2; return shared.x` | 1 | `unknown` | `unknown` | `unknown` | lost precision | | `a.js exports shared / b.js result: return shared.x (no write anywhere)` | 1 | `unknown` | `unknown` | `unknown` | lost precision | | `a.js exports shared, get, result returns shared.x / b.js: export const y = get().x` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const shared = { x: 1, inner: { y: 1 } }; const get = () => shared.inner; get().y = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const make = (v) => ({ x: v }); make(1).x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `(() => ({ x: 1 }))().x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const get = async () => shared; get().x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `function* g() { yield shared; } g().x = 2;` | 1 | `literal 1` | `literal 1` | `literal 1` | correct | | `const other = { x: 1 }; let get = () => shared; get = () => other; get().x = 2;` | 1 | `literal 1` | `literal 1` | `unknown` | lost precision | </details> <details> <summary>Commands</summary> - Real JS: each case runs in its own `node` child. ESM uses `import()`, CJS uses `require`. - CLI: `rea analyze-javascript-application <dir> --artifact-format directory --json`. - MCP: one stdio server per tree and Node, calling `analyze_javascript_application`. - I read the `result` shape from the module that defines it. </details> <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), main `4e3d3354`, #1468 head `9c559f3e` </details> 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

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

TL;DR: #1471 fixed this. Every call form from my report now gives an honest unknown. Getters still give the stale literal 1, so I filed that as #1494.

The merged commit 782e0147c has the same patch I tested at 8414ee49. My full results are in the #1471 comment.

Form Real JS main before main now
get().x = 2, and the slot forms from this report 2 literal 1 unknown
Methods, .bind, await, generators 2 literal 1 unknown
box.v.x = 2 through a getter 2 literal 1 literal 1

Two side effects came with the fix:

  • Read-only calls now give unknown too, as the PR says they may.
  • One shape got much slower. I filed that as #1495.

Thanks, N0zoM1z0, for the broad fix, and thanks, akushonkamen, for the first one in #1468.

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6095354009 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** #1471 fixed this. Every call form from my report now gives an honest `unknown`. Getters still give the stale `literal 1`, so I filed that as #1494. The merged commit `782e0147c` has the same patch I tested at `8414ee49`. My full results are in [the #1471 comment](https://github.com/morluto/rea/pull/1471#issuecomment-6095203282). | Form | Real JS | main before | main now | |---|---|---|---| | `get().x = 2`, and the slot forms from this report | 2 | `literal 1` | `unknown` | | Methods, `.bind`, `await`, generators | 2 | `literal 1` | `unknown` | | `box.v.x = 2` through a getter | 2 | `literal 1` | `literal 1` | Two side effects came with the fix: - Read-only calls now give `unknown` too, as the PR says they may. - One shape got much slower. I filed that as #1495. Thanks, N0zoM1z0, for the broad fix, and thanks, akushonkamen, for the first one in #1468. 🤖 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#491
No description provided.