Repository navigation
Shadow the closure, operator and control-flow handlers natively - #6532
Merged
Merged
Conversation
…osureParameterResolver natively Ports the closure type and parameter resolution: getClosureType() with its body walks (native node callbacks capturing the gathered lists by reference), the two build*() methods, getDeclaredClosureType(), freeVariableRoots() over the native node finder, and the per-file $cachedTypes cache (twin's shape, updated in place). Exports pt_closure_type_resolver_*(), pt_closure_parameter_resolver_resolve() / _resolve_callable_type_for_scope() and pt_contextual_closure_parameter_resolver_*(); native callers get the two parameter lists without the ClosureParameterTypes object (class-map key closureParameterTypes, built for PHP callers only). MutatingScope, ArgumentsHandler and FuncCallHandler (getDeclaredClosureType()) call the entries; adds MutatingScope entries (getFunctionType, getClosureScopeCacheKey, enter*WithoutReflection, getKeepVoidType, ...), the NativeParameterReflection toOptional()/union() entries and ThrowPoint / ImpurePoint / StatementResult readers. SimpleThrowPoint, InitializerExprTypeResolver/Context and TemplateArgumentStats stay PHP (cached sites). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…ir result classes natively Ports the closure / arrow function walk: processClosureNode() with the by-ref use convergence and recorded-pass replay, processArrowFunctionNode(), processImmediatelyCalledCallable(), the two handlers (registered handler entries) and the ProcessClosureResult / ProcessArrowFunctionResult value classes (differential in tests/analyser-values.php). The gatherer frames are native closures (the closure gatherer captures its lists and $closureScope by reference; its scope identity check compares MutatingScope slots). Exports pt_closure_processor_*(), pt_process_*_result_new() and slot readers, pt_closure_handler_get_variable_flow(), pt_arrow_function_handler_get_variable_flow(); adds MutatingScope enterAnonymousFunction/enterArrowFunction/processClosureScope/ withAnonymousFunctionReflection, storage mergeResults, NodeScopeResolver isReplayableConvergenceBody/replayRecording and VariableFlow::arrow entries. ArgumentsHandler now calls the whole closure cluster directly, FuncCallHandler processImmediatelyCalledCallable() too. Class-map keys added: inClosureNode, inArrowFunctionNode (closureReturnStatementsNode reused); removed: closureHandler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…naryHandler natively The boolean operator and ternary handlers with their type and specify-types callbacks as native closures, registered as handler entries; the ternary's (cond && if) || (!cond && else) decomposition builds its operand callbacks natively, the $aFalseyScope memo as a by-reference capture. Shared operand reads, array_merge and boolean verdicts (exact ConstantBooleanType / BooleanType answered without a call) live in OperatorHandlers.h; type callbacks continue on a fresh stack when the C stack runs low. New entries: pt_expression_result_get_specified_types / _answers_on_scope and pt_ternary_handler_get_captured_results, which AssignHandler now calls. Class-map keys booleanAndNode, booleanOrNode, binaryOpExpr, booleanNotExpr. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu # Conflicts: # turbo-ext/config.w32 # turbo-ext/src/main.cpp # turbo-ext/src/support.cpp # turbo-ext/src/support.h # turbo-ext/tests/smoke.php # Conflicts: # build/PHPStan/Build/TurboAttributeCollector.php # turbo-ext/config.w32 # turbo-ext/src/main.cpp # turbo-ext/src/support.cpp # turbo-ext/src/support.h # turbo-ext/tests/smoke.php # Conflicts: # turbo-ext/config.w32 # turbo-ext/src/main.cpp # turbo-ext/src/support.cpp # turbo-ext/src/support.h # turbo-ext/tests/smoke.php # Conflicts: # turbo-ext/config.w32 # turbo-ext/src/main.cpp # turbo-ext/src/support.cpp # turbo-ext/src/support.h # turbo-ext/tests/smoke.php
The arithmetic, bitwise, concatenation and comparison handler with its type, operand-reader, specify-types and identical-type callbacks as native closures, registered as the handler entry. The operator is classified once per class entry (the twin's instanceof chain, memoized per request); a concatenation's type spells out getConcatType() (both operand reads, then resolveConcatType()) so deep `.` chains recurse without an engine frame per level, and every type callback continues on a fresh C stack when the current one runs low. Call arguments are read through pt_call_like_args() in the twin's order. InitializerExprTypeResolver, RicherScopeGetTypeHelper, ImplicitToStringCallHelper and CountNarrowingHelper stay cached sites. Class-map keys for the BinaryOp subclasses the twin tests (smallerExpr .. pipeExpr; Plus, Minus and NotIdentical reuse binaryOpPlus / binaryOpMinus / binaryOpNotIdentical). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu # Conflicts: # turbo-ext/config.w32 # Conflicts: # turbo-ext/config.w32 # Conflicts: # turbo-ext/config.w32 # Conflicts: # turbo-ext/config.w32
The `??` handler with its type, specify-types, create-types and isSet() verdict callbacks as native closures, registered as the handler entry, and the composition helper it shares with AssignOpHandler's `??=`, which now calls it directly (composeType()'s $leftIsSetType inlined, its resolution on a fresh C stack when the current one runs low). New entries: pt_coalesce_composition_helper_get_falsey_specified_types / _get_right_side_scope_specified_types / _compose_type. EnsuredNonNullabilityResult is read through property sites, IssetabilityResolution::isSet() through a cached site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu # Conflicts: # turbo-ext/config.w32
The three jump-statement handlers register their processStmt() bodies as statement-handler entries. break/continue walk their level expression through NodeScopeResolver's direct entry and build the exit point, the break/continue exit flow and the result through the value classes' direct entries; goto's opaque flow through the new pt_variable_flow_all_opaque(). No twin bugs found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The three loop handlers run their convergence passes, record/replay of the fixpoint pass and the post-loop narrowing natively; every body and condition re-walk goes through NodeScopeResolver's direct entries, so the fresh-stack guard stays on the recursion (nested loops around 6,000 nested ifs analyse identically with the extension on and off). Shared pieces live in LoopHandlerCalls.h; LoopWrittenVariableNames::collect() stays PHP (generalization passes only). New entries: pt_mutating_scope_generalize_with_names, _enter_foreach, _enter_foreach_key, _enter_catch_type, _process_finally_scope, _process_always_iterable_foreach_scope_without_pollute; pt_node_scope_resolver_narrow_scope_with_condition; pt_variable_flow_loop, _loop_statement, _switch, _try_catch; pt_variable_flow_builder_child, _writes (the replay, storage-merge and answersOnScope entries of the closure and operator ports are reused). Class map: breaklessWhileLoopNode, doWhileLoopConditionNode, loopWrittenVariableNames (smallerExpr / greaterExpr reused). No twin bugs found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
switch walks its subject, case conditions and case bodies through NodeScopeResolver's direct entries and composes each case's `==` narrowing through IdenticalNarrowingHelper's (looked up from the container per case, like the twin), with narrowScopeWithCondition() as the miss seam. try/catch matches throw points to catch types, recomputes the remaining throw points and merges the finally scope through InternalThrowPoint, InternalStatementExitPoint, MutatingScope (enterCatchType(), processFinallyScope()) and the Type kernel directly. Class map: switchConditionArm, switchConditionNode, catchWithUnthrownExceptionNode, finallyExitPointsNode (equalExpr reused). No twin bugs found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
processStmt() runs natively: the `iteratee !== []` narrowing (its type callback a native closure), the unrolled constant-array iterations, the convergence passes with record/replay, the post-loop iteratee rewrite (mapValueType()/mapKeyType() replacements as native closures), the traversable throw point, the tagged-union destructuring holders and the binding flows. Walks go through NodeScopeResolver's direct entries (foreach around 6,000 nested ifs analyses identically with the extension on and off); VarAnnotationProcessor, AssignHandler::processVirtualAssign(), IdenticalNarrowingHelper and MutatingScope::enterForeach()/enterForeachKey() through theirs. $stmt->getDocComment() is read off the comments attribute. Class map: inForeachNode, foreachValueByRefExpr. Mirrored twin quirk: the post-loop rewrite reads the key's "native" loop type off the PHPDoc scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The fix itself is in place; this observes it. smoke.php checks that a memo hit hands back the operand of the call at hand — for a union() of equal shapes and a remove() of nothing — now that the Type family is native and the checks can be written against it, and the walk-trace corpus gains the reduced reproducer: nested ??= on dim fetches in a foreach, which is what reaches ArrayType::setExistingOffsetValueType() and tells "nothing was written" by identity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN
…ericObjectType Describing $this of an interface whose template bound mentions `static` of the interface itself (bug-13801: `@template TCast of Cast<static>`) recurses without end. The twin describes its type arguments through an array_map() closure, where the engine's C stack check throws "Maximum call stack size ... reached. Infinite recursion?"; the native recursion is direct C++ calls with no such check and overflowed the C stack (SIGSEGV, also on the pre-port Type kernel). The native describe() now checks the stack at the point the twin enters the closure and throws the same Error. The reproducer joins the walk-trace default corpus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
A file the parser rejected with the name resolver's PhpParser\Error (bug-10358's clashing use alias) broke the walk of every other file in the run that looked up a class: the reflection's source locators parse the analysed files on a lookup, and FileNodesFetcher only catches ParserErrorsException. A parse-check child now parses the corpus up front the way the walk does and hands the rejected files to the shard children, which leave them out of the container's analysed paths; each rejected file still gets its own walk, recording FILE-EX. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
ondrejmirtes
force-pushed
the
turbo-native-control-flow-handlers
branch
from
September 22, 2026 12:40
77395a4 to
2c67472
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Shadows the closure path, the operator handlers and the control-flow statement handlers natively. Apart from the
#[ShadowedByTurboExtension]/#[ReferencedByTurboExtension]attributes, the PHP twins are unchanged.Closures and arrow functions
ClosureTypeResolver,ClosureParameterResolverandContextualClosureParameterResolver:getClosureType()with its body walks;getDeclaredClosureType();ClosureProcessor,ClosureHandlerandArrowFunctionHandler, plus their result classes, including the by-referenceuseconvergence and the replay of recorded passes.Operators
BooleanAndHandler,BooleanOrHandler,BooleanNotHandlerandTernaryHandler. The ternary's(cond && if) || (!cond && else)decomposition builds its operand callbacks natively.BinaryOpHandler. The operator kind is looked up once per class, and a concatenation's type is written out so that deep.chains recurse without an engine frame per level.CoalesceHandlerandCoalesceCompositionHelper, whichAssignOpHandler's??=now calls directly.The type callbacks continue on a fresh C stack when the current one runs low.
Control flow
BreakContinueHandler,LabelHandlerandGotoHandler.WhileHandler,DoWhileHandlerandForHandler, including the convergence passes, the record and replay of the fixpoint pass, and the post-loop narrowing.SwitchHandlerandTryCatchHandler.ForeachHandler, including the unrolled constant-array iterations, the post-loop iteratee rewrite and the destructuring holders.Every re-walk goes through
NodeScopeResolver's direct entries, so the fresh-stack guard stays on the recursion path: loops around 6,000 nestedifs analyse identically with the extension on and off.Fixes and coverage
$thisof an interface whose template bound refers tostaticof that same interface (bug-13801) recursed without end. The PHP twin hits the engine's stack check and throws "Maximum call stack size reached", but the native recursion overflowed the C stack instead. NativeGenericObjectType::describe()now checks the stack at the point where the twin enters its closure, and throws the sameError.smoke.phpchecks that aTypeCombinatorCachememo hit hands back the operand of the call at hand. The walk-trace corpus gains the reduced reproducer.walk-trace.phpkeeps files the parser rejects out of the container's analysed paths. Such a file broke the walk of every other file that looked up a class. Each rejected file still gets its own walk.🤖 Generated with Claude Code
https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN