Replay the recorded fixpoint pass instead of repeating the final loop walk - #6249
Conversation
Adopts every before-scope the other storage stored, overwriting existing entries - a finished convergence pass's stores answer for the final walk its replay replaces. Mirrored in the native extension.
… walk Both convergence break paths guarantee the final walk's entry scope equals the last pass's entry, making the final walk an exact repetition of that pass. Convergence passes now record their (node, scope) emissions (RecordingNodeCallback); when the recorded pass's entry is the fixpoint, the final walk is replaced by replaying the recording through the real callback and adopting the pass's storage and statement result. The replay binds the merged storage for its whole duration, so the recorded scopes answer rule asks from the stored before-scopes the same way the repeated walk's per-emission binding would. Applies to While, Do-While, Foreach (non-unrolled finals) and the by-ref closure convergence, whose replay runs through the gathering callback on the pass's own entry scope - the gathering filter compares the anonymous-function reflection by identity. A body containing constructs that analyse differently at deep context than in the top-level final (nested loop/label fixpoints run only at top level, statement-level classes are skipped at deep context) is not replayed; For keeps its real final walk (it applies inferForLoopExpressions there, which the passes do not). Do-While replays the body only - its condition walks stay real. (adapted from commit e0a32b7)
|
Some examples of what this PR skips and where it changes nothing. The shape of the optimizationA top-level loop has always been analysed as N convergence passes (silent, This PR records the last pass's Where it fires$total = 0;
$names = [];
foreach ($items as $item) {
if ($item->isActive()) {
$total += $item->getPrice();
}
$names[] = $item->getName();
}
The same applies to while ($row = $stmt->fetch()) {
$data[] = process($row);
}and to $sum = 0;
$fn = function () use (&$sum) { $sum += $this->next(); };The saving scales with body size × rule count: big loop bodies over rich APIs (WordPress's Where it deliberately does what it always didNested loops in the body (also labels and class-likes) — the body is not replayable: foreach ($matrix as $row) {
foreach ($row as $cell) { // ← outer body excluded
$out[] = $cell * 2;
}
}A nested loop is analysed differently deep vs. top-level (during the outer loop's silent passes the inner loop is walked once at deep context; in the real final walk it runs its own convergence). The recorded emissions would not match what the final walk produces, so Unrolled foreach (['a', 'b', 'c'] as $letter) { ... }Loops that never converge cleanly — the entry-scope equality gate fails and the real final walk runs: $x = 0;
while (cond()) {
$x = [$x]; // ever-deepening type, generalization keeps shifting the entry
}
Why the replay is safeWalking is deterministic in the entry scope. The replay only fires when the final walk's entry scope is |
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
The branch's own fixpoint-replay commits were dropped during the rebase over the merged extraction chunks (#6249); this restores the replay in upstream's final form - raw-recorded pairs wrapped at replay time, the RecordingNodeCallback short-circuit in callNodeCallback - woven into the branch's handler shapes (ambient storage push around pass walks, the deferred While_ statement callback, the on-demand falsey cond re-pricing). replayRecording() binds the storage through the scope's push/pop like every other branch-side ambient binding. The consume mechanism (#6251, closed unmerged) is gone entirely: convergence behavior on this branch is now byte-for-byte upstream's algorithm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
Second extraction chunk from the single-pass analyser branch (follow-up to #6248).
Loop convergence walks the body repeatedly until the entry scope reaches a fixpoint, then walks it once more with the real node callback. When the final walk's entry scope equals the last pass's entry, that final walk reproduces the pass exactly — walking is deterministic in the entry scope. This PR records the last pass's
(node, scope)emissions and its expression-result storage, and when the equality holds, replays the recording through the real callback instead of walking again.RecordingNodeCallbackrecords raw walk scopes — recording pays no callback-scope construction and no storage binding (short-circuited incallNodeCallback); pairs are wrapped viatoNodeCallbackScope()only at replay time.while(cond + body),do-while(body only — its condition walks stay real), non-unrolledforeach, and closure by-ref convergence.isReplayableConvergenceBody()excludes bodies with context-sensitive constructs (nested loops, labels, class-likes) where deep vs top-level analysis differs.ExpressionResultStorage::mergeResults()adopts the recorded pass's stored results into the surrounding storage (PHP twin + turbo mirror, smoke-covered).Benchmarks (user CPU, 3+3 interleaved legs, toggled within one checkout):
--debug src)Analysis output is unchanged (full test suite; turbo on/off self-analysis output byte-identical).
🤖 Generated with Claude Code
https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8