fix(javascript): bound member-mutation paths through reassignment loops

Fixes #1401. Refs #1065. Preserve exact member facts and equal-depth write effects while bounding the observed reassignment-loop expansion.

Validated with the JavaScript semantic/application suite, focused alias regressions, full static checks, real compiled CLI and SDK MCP Notion workflows, and all 20 checks on the final PR head.

Co-Authored-By: Marnix Pluim <286664873+marnixpluim@users.noreply.github.com>
Co-Authored-By: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Marnix Pluim
2026-10-10 06:03:13 +02:00
committed by GitHub
parent 98022a8352
commit 28595276fa
4 changed files with 524 additions and 14 deletions
@@ -211,12 +211,17 @@ const evaluateBinding = (
if (isPrimitive(captured)) value = captured;
}
}
const mutated = binding.mutatedPaths.reduce(
invalidateSemanticMutationPath,
value,
);
const projected = binding.escapedPaths.reduce(
(value, escape) =>
let projected = value;
for (const path of binding.mutatedPaths) {
projected = invalidateSemanticMutationPath(projected, path);
// Every subsequent write returns this same root unknown. Replaying those
// paths at every newly collected effect otherwise becomes quadratic.
if (projected.status === "unknown") break;
}
for (const escape of binding.escapedPaths) {
// Escapes only invalidate object references; other lattice values survive.
if (projected.status !== "object" && projected.status !== "array") break;
if (
context.capturePoint !== undefined &&
semanticEscapeFollowsCapture(
binding,
@@ -224,10 +229,10 @@ const evaluateBinding = (
escape.node,
context.state.parentsByNode,
)
? value
: invalidateSemanticEscapedPath(value, escape.path),
mutated,
);
)
continue;
projected = invalidateSemanticEscapedPath(projected, escape.path);
}
// Point-local memoization also bounds failed speculative captures. Only a
// primitive result is allowed back into ordinary value evaluation.
if (