[GH-ISSUE #2560] fix(javascript-symbolic): nested returns become definite undefined results #877

Closed
opened 2026-10-15 18:29:14 +00:00 by bsakaguchi · 1 comment
Owner

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

Summary

Static analysis of an immediately invoked function loses its returned value when the return is inside a while loop, infinite for loop, try/finally block, or switch statement. The symbolic evaluator reports a definite undefined result without marking the result unresolved or truncated. It must preserve reachable return values or explicitly retain uncertainty when it cannot interpret the enclosing statement. This is a bounded P2 correctness defect; a downstream Electron API recovery probe loses a member candidate but still reports partial analysis.

Steps to reproduce

Use the checkout identified under Runtime or environment with its locked dependencies installed. Add src/domain/javascript/symbolic-return-control-flow.test.ts containing this inert-source test; the test analyzes the supplied JavaScript strings without executing them.

import { expect, it } from "vitest";
import * as t from "@babel/types";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js";
import { completeSemanticSteps, traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { JavaScriptSymbolicBindings } from "./javascriptSymbolicBindings.js";
import type { SymbolicValue } from "./javascriptSymbolicValues.js";

const analyze = (source: string) => {
  const parsed = parseJavaScriptSource(source);
  if (parsed === null) throw new Error("fixture did not parse");
  const state = completeSemanticSteps(indexJavaScriptBindingsSteps(parsed.program));
  const resolver = new JavaScriptSymbolicBindings(state, parsed.program);
  let value: SymbolicValue | undefined;
  traverseJavaScriptAst(parsed.program, {
    enter: (node) => {
      if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "inspect" })) {
        value = resolver.evaluate(node.arguments[0]);
      }
    },
  });
  if (value === undefined) throw new Error("inspect argument missing");
  return { value, unresolved: resolver.unresolved, truncated: resolver.truncated };
};

it.each([
  ["while", 'inspect((() => { while (flag) { return "loop"; } })());'],
  ["for", 'inspect((() => { for (;;) { return "forever"; } })());'],
  ["try", 'inspect((() => { try { return "try"; } finally {} })());'],
  ["switch", 'inspect((() => { switch (x) { default: return "switch"; } })());'],
])("does not invent an unqualified undefined result for %s", (_name, source) => {
  const result = analyze(source);
  expect(result.value.kind === "literal" && result.value.value === undefined
    && !result.unresolved && !result.truncated).toBe(false);
});

it("preserves direct and absent return controls", () => {
  expect(analyze('inspect((() => { return "value"; })());').value)
    .toEqual({ kind: "literal", value: "value" });
  expect(analyze('inspect((() => {})());').value)
    .toEqual({ kind: "literal", value: undefined });
});

Run the focused test from the repository root:

cache_dir="$(mktemp -d)"
NODE_OPTIONS=--max-old-space-size=1536 VITEST_CACHE_DIR="$cache_dir" \
  npm run test:focused -- src/domain/javascript/symbolic-return-control-flow.test.ts
rm -rf "$cache_dir"

Each of the four return-flow checks fails independently. The direct-return and empty-function controls pass. The unresolved names in the while and switch inputs are intentional static unknowns; even if the evaluator cannot resolve those branches, it must not assert an unqualified undefined result.

Expected behavior

For these inputs, static function-call analysis must retain the reachable returned value or mark the interpretation uncertain. The infinite loop and try/finally inputs return their string immediately; the switch default returns its string when its discriminant is available; the while input can return its string or fall through depending on its unresolved condition. Unsupported enclosing statements may remain unknown instead of being fully modeled. A direct return must still resolve to its literal, and an empty function may correctly resolve to undefined.

Actual behavior

Independent focused execution observes { kind: "literal", value: undefined } for every affected input, with both unresolved and truncated false. Four separate assertions fail, while direct-return, empty-function, definitely false loop, and guarded conditional-return controls pass. The existing symbolic binding test file also passes all 26 tests, so those tests do not cover this omission.

A production static-analysis probe exposes an API object returned by an immediately invoked function through contextBridge. Returning { send: () => electron.ipcRenderer.send("returned:value") } directly retains member send and candidate call path api.send. Placing that return inside an infinite for loop instead yields no recovered members, unknown_members 1, and no candidate call path. Both outputs are partial and retain general uncertainty limitations; this observation does not establish a falsely complete application analysis or absence of the real API member.

Evidence

  • Expected source: AGENTS.md:5 and AGENTS.md:19 require distinguishing unknowns and preserving uncertainty at interpretation boundaries; src/domain/javascript/javascriptSymbolicBindings.test.ts:322 demonstrates guarded return alternatives under an unresolved condition.
  • Failure source: src/domain/javascript/javascriptSymbolicBindings.ts:678 sends ordinary block-bodied function calls into return analysis; :708 inserts undefined for fallthrough; :721 skips the four enclosing statement types and reaches :815 without returned values or uncertainty. Independent focused execution records the values and flags described under Actual behavior.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The reproduction evaluates ordinary nonrecursive arrow functions with no mutation, module loader, or resource-limit trigger. The evaluator's active-function and resource guards do not reject these inputs. The production consumer is Electron wrapped-call collection through analyzeJavaScriptStaticSource; no CLI/MCP process, Electron application, or unrelated export-return-shape analyzer was exercised.

Runtime or environment

The failure was verified at source baseline: a2d8b94dd5 and is still present at 8b181716ac. Node.js v26.10.0, npm 11.19.1, Vitest 5.0.3, and existing dependencies installed from the lockfile. Verification used focused domain tests with a 1536 MiB V8 heap cap. The six-test verifier probe exited 1 with four failures and two passing combined tests; the existing focused test file exited 0 with 26 passing tests.

Restoration check

Analyze the four inputs without inventing an unqualified undefined result. Preserve their reachable returned strings, with branch/fallthrough guards where needed, or explicitly report uncertainty for unsupported statements. A correctly recovered string literal is a valid result; returning undefined with truthful unresolved coverage is not the same false certainty as the observed failure. Keep direct-return, empty-function, definitely false loop, and conditional-return controls correct. If the production API probe is modeled precisely, retain its returned member and candidate path; otherwise keep its recovery uncertainty explicit rather than claiming those facts are absent. No particular implementation strategy is required.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Four ordinary static function-body shapes lose reachable return information, and an exercised production consumer loses an API-member candidate. The observed consumer retains partial coverage; no severe harm, data corruption, or falsely complete public workflow result was established.
  • Workaround status: unverified
  • Workaround basis: No practical same-artifact recovery workflow was verified. Rewriting a returned API object into a direct return makes the control pass but changes the analyzed artifact, so it does not establish an available workaround for callers who possess only the original bundle.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2560 ## Summary Static analysis of an immediately invoked function loses its returned value when the return is inside a while loop, infinite for loop, try/finally block, or switch statement. The symbolic evaluator reports a definite undefined result without marking the result unresolved or truncated. It must preserve reachable return values or explicitly retain uncertainty when it cannot interpret the enclosing statement. This is a bounded P2 correctness defect; a downstream Electron API recovery probe loses a member candidate but still reports partial analysis. ## Steps to reproduce Use the checkout identified under Runtime or environment with its locked dependencies installed. Add `src/domain/javascript/symbolic-return-control-flow.test.ts` containing this inert-source test; the test analyzes the supplied JavaScript strings without executing them. ```ts import { expect, it } from "vitest"; import * as t from "@babel/types"; import { parseJavaScriptSource } from "./javascriptSourceParser.js"; import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js"; import { completeSemanticSteps, traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; import { JavaScriptSymbolicBindings } from "./javascriptSymbolicBindings.js"; import type { SymbolicValue } from "./javascriptSymbolicValues.js"; const analyze = (source: string) => { const parsed = parseJavaScriptSource(source); if (parsed === null) throw new Error("fixture did not parse"); const state = completeSemanticSteps(indexJavaScriptBindingsSteps(parsed.program)); const resolver = new JavaScriptSymbolicBindings(state, parsed.program); let value: SymbolicValue | undefined; traverseJavaScriptAst(parsed.program, { enter: (node) => { if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "inspect" })) { value = resolver.evaluate(node.arguments[0]); } }, }); if (value === undefined) throw new Error("inspect argument missing"); return { value, unresolved: resolver.unresolved, truncated: resolver.truncated }; }; it.each([ ["while", 'inspect((() => { while (flag) { return "loop"; } })());'], ["for", 'inspect((() => { for (;;) { return "forever"; } })());'], ["try", 'inspect((() => { try { return "try"; } finally {} })());'], ["switch", 'inspect((() => { switch (x) { default: return "switch"; } })());'], ])("does not invent an unqualified undefined result for %s", (_name, source) => { const result = analyze(source); expect(result.value.kind === "literal" && result.value.value === undefined && !result.unresolved && !result.truncated).toBe(false); }); it("preserves direct and absent return controls", () => { expect(analyze('inspect((() => { return "value"; })());').value) .toEqual({ kind: "literal", value: "value" }); expect(analyze('inspect((() => {})());').value) .toEqual({ kind: "literal", value: undefined }); }); ``` Run the focused test from the repository root: ```sh cache_dir="$(mktemp -d)" NODE_OPTIONS=--max-old-space-size=1536 VITEST_CACHE_DIR="$cache_dir" \ npm run test:focused -- src/domain/javascript/symbolic-return-control-flow.test.ts rm -rf "$cache_dir" ``` Each of the four return-flow checks fails independently. The direct-return and empty-function controls pass. The unresolved names in the while and switch inputs are intentional static unknowns; even if the evaluator cannot resolve those branches, it must not assert an unqualified undefined result. ## Expected behavior For these inputs, static function-call analysis must retain the reachable returned value or mark the interpretation uncertain. The infinite loop and try/finally inputs return their string immediately; the switch default returns its string when its discriminant is available; the while input can return its string or fall through depending on its unresolved condition. Unsupported enclosing statements may remain unknown instead of being fully modeled. A direct return must still resolve to its literal, and an empty function may correctly resolve to undefined. ## Actual behavior Independent focused execution observes `{ kind: "literal", value: undefined }` for every affected input, with both `unresolved` and `truncated` false. Four separate assertions fail, while direct-return, empty-function, definitely false loop, and guarded conditional-return controls pass. The existing symbolic binding test file also passes all 26 tests, so those tests do not cover this omission. A production static-analysis probe exposes an API object returned by an immediately invoked function through contextBridge. Returning `{ send: () => electron.ipcRenderer.send("returned:value") }` directly retains member `send` and candidate call path `api.send`. Placing that return inside an infinite for loop instead yields no recovered members, unknown_members 1, and no candidate call path. Both outputs are partial and retain general uncertainty limitations; this observation does not establish a falsely complete application analysis or absence of the real API member. ## Evidence - Expected source: `AGENTS.md:5` and `AGENTS.md:19` require distinguishing unknowns and preserving uncertainty at interpretation boundaries; `src/domain/javascript/javascriptSymbolicBindings.test.ts:322` demonstrates guarded return alternatives under an unresolved condition. - Failure source: `src/domain/javascript/javascriptSymbolicBindings.ts:678` sends ordinary block-bodied function calls into return analysis; `:708` inserts undefined for fallthrough; `:721` skips the four enclosing statement types and reaches `:815` without returned values or uncertainty. Independent focused execution records the values and flags described under Actual behavior. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The reproduction evaluates ordinary nonrecursive arrow functions with no mutation, module loader, or resource-limit trigger. The evaluator's active-function and resource guards do not reject these inputs. The production consumer is Electron wrapped-call collection through `analyzeJavaScriptStaticSource`; no CLI/MCP process, Electron application, or unrelated export-return-shape analyzer was exercised. ## Runtime or environment The failure was verified at source baseline: a2d8b94dd50dab8d966239dd63ab84718aa8b4b4 and is still present at 8b181716aca2cb467be499cba226abfb5ae19141. Node.js v26.10.0, npm 11.19.1, Vitest 5.0.3, and existing dependencies installed from the lockfile. Verification used focused domain tests with a 1536 MiB V8 heap cap. The six-test verifier probe exited 1 with four failures and two passing combined tests; the existing focused test file exited 0 with 26 passing tests. ## Restoration check Analyze the four inputs without inventing an unqualified undefined result. Preserve their reachable returned strings, with branch/fallthrough guards where needed, or explicitly report uncertainty for unsupported statements. A correctly recovered string literal is a valid result; returning undefined with truthful unresolved coverage is not the same false certainty as the observed failure. Keep direct-return, empty-function, definitely false loop, and conditional-return controls correct. If the production API probe is modeled precisely, retain its returned member and candidate path; otherwise keep its recovery uncertainty explicit rather than claiming those facts are absent. No particular implementation strategy is required. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Four ordinary static function-body shapes lose reachable return information, and an exercised production consumer loses an API-member candidate. The observed consumer retains partial coverage; no severe harm, data corruption, or falsely complete public workflow result was established. - Workaround status: unverified - Workaround basis: No practical same-artifact recovery workflow was verified. Rewriting a returned API object into a direct return makes the control pass but changes the analyzed artifact, so it does not establish an available workaround for callers who possess only the original bundle.
bsakaguchi 2026-10-15 18:29:14 +00:00
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Addressed on main in 896696b2e761e34c267977bb5c6982c607a622d0.

Owned returns in unsupported loop/switch/try control flow remain unknown rather than becoming definite undefined; nested callable returns retain their own ownership.

Validation: Affected suites passed in the merged 65-file run (1169 passed, 2 explicit skips), with full static/architecture checks and documentation checks passing. Final installed-package verification is recorded separately in the audit.

Verification scope: Controlled source and production-boundary verification on macOS with pinned Node 24.18.0; real external engines and Windows hosts were not independently rerun. Closure covers the described source defect, not broader provider conformance or performance.

<!-- gh-comment-id:6159190644 --> @morluto commented on GitHub (Oct 14, 2026): Addressed on main in [896696b2e761e34c267977bb5c6982c607a622d0](https://github.com/morluto/rea/commit/896696b2e761e34c267977bb5c6982c607a622d0). Owned returns in unsupported loop/switch/try control flow remain unknown rather than becoming definite undefined; nested callable returns retain their own ownership. Validation: Affected suites passed in the merged 65-file run (1169 passed, 2 explicit skips), with full static/architecture checks and documentation checks passing. Final installed-package verification is recorded separately in the audit. Verification scope: Controlled source and production-boundary verification on macOS with pinned Node 24.18.0; real external engines and Windows hosts were not independently rerun. Closure covers the described source defect, not broader provider conformance or performance.
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#877
No description provided.