[PR #1468] [CLOSED] fix(javascript): attribute writes through a simple thunk's call result #2907

Closed
opened 2026-10-17 21:11:40 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/1468
Author: @akushonkamen
Created: 10/10/2026
Status: ❌ Closed

Base: main ← Head: fix/1449-call-expr-referenced-values


📝 Commits (1)

  • 9c559f3 fix(javascript): attribute writes through a simple thunk's call result

📊 Changes

2 files changed (+137 additions, -1 deletions)

View changed files

📝 src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts (+37 -0)
📝 src/domain/javascript/javascriptSemanticMemberMutations.ts (+100 -1)

📄 Description

Fixes #1449

Root cause

referencedValues in src/domain/javascript/javascriptSemanticMemberMutations.ts handled Object/Array/Assignment/Await/Sequence/Conditional/Logical bases, but had no CallExpression branch: a call base such as get() fell through to the empty return [] fallback. For get().x = 2 with const get = () => shared, the write therefore never attributed to shared, binding.mutatedPaths stayed empty, the evaluation side never invalidated the cache (evaluateBinding walks mutatedPaths in javascriptSemanticValues.ts), and static_return_shapes kept reporting literal 1 while coverage claimed complete — a fact real JS cannot produce.

Reproduction

The issue's fixture (get().x = 2 through a thunk) reported literal 1; a return shape of literal 2/unknown is the truth. Equivalent forms const t = get(); t.x = 2, function get() { return shared; }, IIFE, member-chain thunks (() => shared.inner), delete and ++ targets all reproduced. Reproduced as 8 failing unit cases before the fix (red), all passing after (green).

Fix

referencedValues gains a CallExpression/OptionalCallExpression branch that resolves the call through simple local thunks:

  • The callee is a function expression (IIFE) or an identifier whose binding's initializer is a plain function.
  • The function must be synchronous and non-generator — async/generator callees hand back wrappers (promise/iterator), not the returned object, so writes through them do not reach the captured binding.
  • Each return at the function's own top level (skipping nested functions/classes) must be an identifier or a member chain with exact static keys rooted in a captured binding.

Resolved roots re-enter the existing alias walk as exact paths ([...chainKeys, ...initializerProjection, ...path]), where the per-origin binding lifetime already closes cycles — no fixed-cap pruning, consistent with the exact-path semantics established in #1390/#1445. A returned call is deliberately left unexpanded, so thunk chains cannot loop the pending queue.

Tests

src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts:

  • 8 new cases assert the write-through result becomes unknown: arrow thunk, stored call result, function declaration, function expression, IIFE, member-chain thunk, delete, ++.
  • 2 new guards assert no over-invalidation: a thunk returning a fresh object literal and an async thunk keep shared known.
  • Targeted file: 163 passed (was 8 failed / 155 passed before).
  • JavaScript domain project: 39 files, 739 passed.
  • Fast projects (domain, services, adapters, composition, conformance, evaluation): 330 files, 3634 passed, 7 skipped, 0 failed.
  • tsc --noEmit clean; prettier/oxfmt/oxlint clean.

Known remaining gap (not a regression)

Forms that park the call result in a property slot first (o.a = get(); o.a.x = 2) still need slot-level value flow to attribute the later write; this change does not alter their behavior. They are listed under the issue's own "Not run" scope and need a slot-carried-reference mechanism, which I'd propose as a follow-up.

Disclosure: prepared with AI assistance (ZCode agent, GLM), reviewed and tested by a human.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/1468 **Author:** [@akushonkamen](https://github.com/akushonkamen) **Created:** 10/10/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/1449-call-expr-referenced-values` --- ### 📝 Commits (1) - [`9c559f3`](https://github.com/morluto/rea/commit/9c559f3e95ac41f6f986a1b215c633cc4329452c) fix(javascript): attribute writes through a simple thunk's call result ### 📊 Changes **2 files changed** (+137 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts` (+37 -0) 📝 `src/domain/javascript/javascriptSemanticMemberMutations.ts` (+100 -1) </details> ### 📄 Description Fixes #1449 ## Root cause `referencedValues` in `src/domain/javascript/javascriptSemanticMemberMutations.ts` handled Object/Array/Assignment/Await/Sequence/Conditional/Logical bases, but had no `CallExpression` branch: a call base such as `get()` fell through to the empty `return []` fallback. For `get().x = 2` with `const get = () => shared`, the write therefore never attributed to `shared`, `binding.mutatedPaths` stayed empty, the evaluation side never invalidated the cache (`evaluateBinding` walks `mutatedPaths` in `javascriptSemanticValues.ts`), and `static_return_shapes` kept reporting `literal 1` while coverage claimed `complete` — a fact real JS cannot produce. ## Reproduction The issue's fixture (`get().x = 2` through a thunk) reported `literal 1`; a return shape of `literal 2`/`unknown` is the truth. Equivalent forms `const t = get(); t.x = 2`, `function get() { return shared; }`, IIFE, member-chain thunks (`() => shared.inner`), `delete` and `++` targets all reproduced. Reproduced as 8 failing unit cases before the fix (red), all passing after (green). ## Fix `referencedValues` gains a `CallExpression`/`OptionalCallExpression` branch that resolves the call through **simple local thunks**: - The callee is a function expression (IIFE) or an identifier whose binding's initializer is a plain function. - The function must be synchronous and non-generator — async/generator callees hand back wrappers (promise/iterator), not the returned object, so writes through them do not reach the captured binding. - Each `return` at the function's own top level (skipping nested functions/classes) must be an identifier or a member chain with exact static keys rooted in a captured binding. Resolved roots re-enter the existing alias walk as exact paths (`[...chainKeys, ...initializerProjection, ...path]`), where the per-origin binding lifetime already closes cycles — no fixed-cap pruning, consistent with the exact-path semantics established in #1390/#1445. A returned call is deliberately left unexpanded, so thunk chains cannot loop the pending queue. ## Tests `src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts`: - 8 new cases assert the write-through result becomes `unknown`: arrow thunk, stored call result, function declaration, function expression, IIFE, member-chain thunk, `delete`, `++`. - 2 new guards assert no over-invalidation: a thunk returning a fresh object literal and an `async` thunk keep `shared` known. - Targeted file: **163 passed** (was 8 failed / 155 passed before). - JavaScript domain project: **39 files, 739 passed**. - Fast projects (domain, services, adapters, composition, conformance, evaluation): **330 files, 3634 passed, 7 skipped, 0 failed**. - `tsc --noEmit` clean; prettier/oxfmt/oxlint clean. ## Known remaining gap (not a regression) Forms that park the call result in a property slot first (`o.a = get(); o.a.x = 2`) still need slot-level value flow to attribute the later write; this change does not alter their behavior. They are listed under the issue's own "Not run" scope and need a slot-carried-reference mechanism, which I'd propose as a follow-up. Disclosure: prepared with AI assistance (ZCode agent, GLM), reviewed and tested by a human. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-17 21:11:40 +00:00
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#2907
No description provided.