Skip to content

Feed engine gatherers from NodeScopeResolver frames instead of wrapping node callbacks - #6258

Merged
ondrejmirtes merged 1 commit into
2.2.xfrom
gathering-cleanup
Aug 24, 2026
Merged

ondrejmirtes merged 1 commit into
2.2.xfrom
gathering-cleanup

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Post-fiber cleanup: with node callbacks synchronous since #6248, the gatherer/rule-facing callback split no longer needs to ride the callback channel.

GatheringNodeCallback paired an engine-feeding gatherer (return statements, execution ends, impure points, invalidate expressions) with the rule-facing callback; callNodeCallback() unwrapped the chain per emission so gatherers got the raw walk scope, and VirtualAssignNodeCallback had to rebuild chains so its filter could not starve a gatherer.

Now gatherers live as frames on NodeScopeResolver:

  • callNodeCallback() feeds every frame the raw walk scope directly — same per-emission cost profile as the old unwrap, no NodeCallbackScope construction for engine code.
  • replayRecording() feeds frames for replayed emissions too. This also fixes a quirk: a gatherer reached through a replayed recording previously observed the storage-backed scope instead of the raw one (output-neutral either way — verified).
  • The nine construction sites (closures, arrow functions, property hooks, class methods, functions, expression statements, anonymous-class new, array_walk args) push a frame around their body walk and pass the rule-facing callback through untouched.
  • Fresh walks started mid-analysis by extensions (processNodes()/processStmtNodes()) suspend the frames, matching the old behavior where such walks carried no wrapper.
  • VirtualAssignNodeCallback::create() loses the chain-rebuild; the ShallowNodeCallback marker interface goes too — nothing has consulted it since the fiber era.

Analysis output is byte-identical on full self-analysis vs the base; full test suite, make phpstan, and CS green. No turbo-shadowed classes touched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8

…ng node callbacks

GatheringNodeCallback existed so engine-feeding gatherers (return
statements, execution ends, impure points, invalidate expressions) could
ride the node-callback channel while only the rule-facing remainder got
the storage-backed scope - callNodeCallback() unwrapped the chain per
emission, and VirtualAssignNodeCallback had to rebuild chains so a
filter could not starve a gatherer.

With everything synchronous the split is unnecessary: gatherers live as
frames on NodeScopeResolver, fed the raw walk scope directly by
callNodeCallback() and by replayRecording() for replayed emissions
(previously a replayed gatherer saw the storage-backed scope - the raw
scope is state-identical and consistent with live emissions). The nine
construction sites push a frame around their body walk and pass the
rule-facing callback through untouched. Fresh walks extensions start
mid-analysis (processNodes()/processStmtNodes()) suspend the frames, so
a rule-started re-walk cannot feed the interrupted walk's gatherers -
matching the old behavior where such walks carried no wrapper.

The ShallowNodeCallback marker went with it: nothing has consulted it
since the fiber era.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
@ondrejmirtes
ondrejmirtes merged commit e288e03 into 2.2.x Aug 24, 2026
441 of 442 checks passed
@ondrejmirtes
ondrejmirtes deleted the gathering-cleanup branch August 24, 2026 20:41
ondrejmirtes added a commit that referenced this pull request Aug 24, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes added a commit that referenced this pull request Aug 24, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes added a commit that referenced this pull request Aug 30, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes added a commit that referenced this pull request Aug 30, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes added a commit that referenced this pull request Aug 31, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes added a commit that referenced this pull request Aug 31, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes added a commit that referenced this pull request Sep 1, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
ondrejmirtes added a commit that referenced this pull request Sep 2, 2026
The rebase over the merged gathering cleanup (#6258) kept the branch's
wrapper-based gatherer sites while the base deleted
GatheringNodeCallback; this converts them to the upstream frame
mechanism - pushNodeGatherer()/popNodeGatherer() on NodeScopeResolver,
fed the raw walk scope by callNodeCallback() and per replayed pair by
replayRecording() - woven into the branch's shapes: the per-body
storages of method/function bodies, the ambient storage pushes, and the
scope-bound replayRecording() signature. Gatherer bodies are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnwgpaeUXRkgSDyg95tfK8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant