Skip to content

Shadow the call, assignment and core statement handlers natively - #6523

Merged
ondrejmirtes merged 14 commits into
2.3.xfrom
turbo-native-call-and-statement-handlers
Sep 22, 2026
Merged

ondrejmirtes merged 14 commits into
2.3.xfrom
turbo-native-call-and-statement-handlers

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Shadows the call handlers, the assignment handlers and the hottest statement handlers natively, together with the method reflections, impure points and variable-flow classes they create. Apart from the #[ShadowedByTurboExtension] / #[ReferencedByTurboExtension] attributes, the PHP twins are unchanged.

Expression handlers

  • MethodCallHandler, StaticCallHandler and NewHandler. Their type, specify-types, create-types and assert-mapping callbacks are native closures, and each handler is registered as its class's handler entry. The PHP collaborators and value helpers the three share (argument handling, acceptor selection, template resolution against an acceptor) live in CallHandlerSupport.h.

  • DynamicReturnTypeStoragePrimer, which primes the storage for dynamic return-type extensions.

  • AssignHandler and AssignOpHandler, each as one port:

    • every target kind in prepareTarget() and every write kind in applyWrite();
    • the conditional-expression machinery;
    • the offset-chain composition.

    Both split processExpr(), so only the walk state sits on the recursion path.

  • PhpParser call arguments (getRawArgs(), isFirstClassCallable(), getArgs()) are read from the args slot. A class that overrides those methods still gets its methods called.

Statement handlers

  • ExpressionHandler, ReturnHandler, EchoHandler, BlockHandler and NopHandler.
  • ClassMethodHandler, FunctionHandler and ClassLikeHandler. The body gatherers, which run for every node a method or function body emits, are native closures. Class members are sorted with the twin's comparator and usort()'s stable fallback.
  • IfHandler.

Values and helpers

  • ResolvedMethodReflection and ChangedTypeMethodReflection, the wrapper pair $scope->getMethodReflection() returns for nearly every method call. pt_extended_method_reflection_call() dispatches to the native body when it can.
  • SimpleImpurePoint, the impure point every call handler derives from the callee's reflection.
  • The four final variable-flow classes, as native subclasses of the native VariableFlow.
  • VarAnnotationProcessor. A statement without a doc comment, the common case, is answered without a call.

Deep nesting

VariableLivenessResolver and VariableFlowBuilder recurse as deep as the flow tree nests: a 10000-operand $a + $a + ... is a 10000-deep sequence. The PHP twin recursed on the VM stack, so the native walk could overflow the C stack. They now continue on a fresh stack through pt_engine_with_stack() when the current one runs low.

Verification

With the extension loaded and active:

  • side-by-side.php, signature-parity.php and smoke.php pass, and walk-trace.php reports identical traces (612,167 lines).
  • make tests, make phpstan, make cs, make lint, make lint-turbo and make sanitize-turbo are clean.
  • make pgo with the strict flags passes under GCC 11.4 (CI's gnu image) against PHP 8.4 and PHP 8.6.

🤖 Generated with Claude Code

https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN

ondrejmirtes and others added 14 commits September 22, 2026 13:06
The method reflection $scope->getMethodReflection() returns for nearly every
method call: ResolvedMethodReflection over ChangedTypeMethodReflection over the
PHP reflection. Both keep the twins' slots and memoization; a delegation to a
native ChangedTypeMethodReflection takes its body directly, any other wrapped
reflection one cached method site per member. pt_extended_method_reflection_call()
is that dispatch for any method reflection (the handlers' entry), next to
pt_resolved_method_reflection_new() / pt_changed_type_method_reflection_new(),
which the prototypes in TypeTraits.cpp now use. Class-map keys
resolvedMethodReflection / changedTypeMethodReflection are gone,
resolvedFunctionVariantWithOriginal is new. Differential in type-family.php.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The impure point every call handler derives from the callee's reflection:
the value class plus createFromVariant() and
resolvePureUnlessCallableIsImpureVerdict(), asking the method reflections
through pt_extended_method_reflection_call(), the scope through its getType()
entry and the Types through their ops. pt_simple_impure_point_resolve()
answers createFromVariant() without the intermediate object for native
handlers; pt_simple_impure_point_new() replaces the class-map instantiation in
TypeTraits.cpp (key simpleImpurePoint removed) and ClosureType::isPure() reads
isCertain() from the slot. New ClassReflection entries getDisplayName() /
isBuiltin(). Differential in type-family.php.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The method call handler with its type, specify-types, create-types and
assert-mapping callbacks as native closures, registered as the class's handler
entry. NodeScopeResolver, DefaultNarrowingHelper, TypeSpecifier and the
early-terminating / return-type / throw-point helpers are reached through direct
entries (new: pt_early_terminating_call_helper_is_early_terminating_method_call,
pt_method_call_return_type_helper_method_call_return_type,
pt_method_throw_point_helper_get_throw_point,
pt_type_specifier_get_method_type_specifying_extensions_for_class), as are the
method reflections, SimpleImpurePoint, the primer (push/pop without closures,
also for MethodCallReturnTypeHelper) and new MutatingScope / ExpressionResult /
VariableFlow(Builder) / TypeUtils entries; ArgumentsHandler stays a cached site.
The inline containsNullsafe() reader replaces the out-of-line entry. Class-map
key invalidateExprNode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The whole twin in one port: processExpr(), processVirtualAssign(),
prepareTarget() with every target kind and applyWrite() with every write
kind, the conditional-expression machinery (falsey sentinels, ternary arms,
match, in_array, derived holders), the offset-chain composition and the
narrowing closures as native closures; AssignOpHandler calls the native
prepareTarget()/applyWrite() directly, and both split processExpr() so only
the walk state sits on the recursion path. Exports
pt_assign_handler_prepare_target / _apply_write / _process_virtual_assign,
plus direct entries on MutatingScope (8), NodeScopeResolver (3),
VariableFlowBuilder (4), VariableFlow (inputs, choice), NonNullabilityHelper
and MethodThrowPointHelper. TemplateArgumentObserver, VarAnnotationProcessor,
the other handlers' composers and the reflections stay PHP behind one helper
each. Twin oddities are mirrored (the reversed-index dim-fetch pairing in
produceArrayDimFetchAssignValueToWrite() among them).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…d NopHandler natively

The hottest statement handlers register their processStmt() bodies as
statement-handler entries. ExpressionHandler's gatherer frame is a native
closure capturing $currentScope and &$hasAssign; NodeScopeResolver and
StatementsHandler are called through their direct entries, the twins'
try/finally through pt_finally(). ImplicitToStringCallHelper stays PHP behind
EchoHandler's cached site. New direct entries: pt_expression_result_get_truthy_scope
/ _get_falsey_scope, pt_variable_flow_exit / _conditional,
pt_mutating_scope_get_anonymous_function_reflection,
pt_node_scope_resolver_push_node_gatherer / _pop_node_gatherer /
_collect_return_send. Mirrors the twin's BlockHandler dropping the inner
variableFlow when not polluting the scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The declaration handlers register statement-handler entries. The body
gatherers (called for every node a method or function body emits) are native
closures with the five gathered lists captured by reference; ClassLikeHandler
sorts the class members with zend_hash_sort() and the twin's comparator plus
usort()'s stable fallback. New direct entries: MutatingScope
enterClassMethod/enterFunction/enterClass, rememberConstructorScope,
invalidateExistenceCheckExpressions, getNamespace; ClassReflection
hasConstructor/getConstructor/isReadOnly/getFileName/evictPrivateSymbols;
VariableLivenessResolver::resolve; ClassStatementsGatherer new/getters;
StatementResult slot readers. The declaration processors (AttributesHandler,
PhpDocsResolver, ParametersProcessor, DeprecatedAttributeResolver) stay PHP
behind shared cached sites in StmtHandlerCalls.h.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Registers the if/elseif/else handler as a statement-handler entry. The
condition results, branch scopes and flows go through the ExpressionResult,
InternalStatementResult, MutatingScope::mergeWith(), VariableFlow::conditional()
and NodeScopeResolver direct entries; the condition type's
toBoolean()/isTrue()/isFalse() through the native type dispatch, isTrue() and
isFalse() asked once per condition type (the twin asks them repeatedly; they
are pure).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The static call handler with its Closure::bind() scope factory, type,
specify-types, create-types and assert-mapping callbacks as native closures,
registered as the class's handler entry. The PHP collaborator sites and value
helpers it shares with MethodCallHandler (ArgumentsHandler, ArgumentsNormalizer,
ParametersAcceptorSelector, acceptors, Assertions, array_merge(), the explicit
never check, template resolution against an acceptor) move into
CallHandlerSupport.h; its array_merge() now also keeps the engine's shortcut for
an empty side over a string-keyed map ([] + [] stays the shared empty array).
New entries: pt_mutating_scope_resolve_name / _enter_closure_bind,
pt_class_reflection_is / _is_subclass_of_class,
pt_type_specifier_get_static_method_type_specifying_extensions_for_class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The new handler with its anonymous-class constructor gatherer (the by-reference
capture a reference zval), type, specify-types, template-map and assert-mapping
callbacks as native closures, registered as the class's handler entry; the
exact-instantiation return type (dynamic static-method return type extensions,
the assigned-property and inherited generic constructor paths) runs natively,
sharing CallHandlerSupport.h with the other call handlers.
New entries: pt_class_reflection_is_final / _as_final / _get_template_type_map /
_get_active_template_type_map / _type_map_to_list / _with_types,
pt_template_type_map_get_types / _get_type / _resolve_to_bounds / _map,
pt_simple_impure_point_resolve_verdict and the inline
pt_statement_result_throw_points() reader. Class-map keys
dummyConstructorReflection and genericTypeTemplateTraverser.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…d VariableControlFlow natively

The four final flow classes become native subclasses of the native
VariableFlow, their state in the twin's promoted readonly slots. The
VariableFlow factories construct them directly (pt_variable_access_flow_new,
pt_variable_sequence_flow_new, pt_variable_input_flow_new,
pt_variable_control_flow_new) instead of calling the PHP constructors, and
VariableLivenessResolver / VariableFlowBuilder read the slots by constant
offset after a class-entry compare. Constructors called from PHP keep the
twin's assignment order and readonly errors (pt_variable_flow_init_readonly,
which also names the declaring class for VariableFlow::$kind now). Drops the
class-map keys variableAccessFlow, variableSequenceFlow, variableControlFlow
and variableInputFlow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
getRawArgs(), isFirstClassCallable() and getArgs() of a FuncCall,
MethodCall, NullsafeMethodCall, StaticCall or New_ are answered from the
`args` slot by pt_call_like_raw_args(), pt_call_like_is_first_class_callable()
and pt_call_like_args() (support.cpp), after a per-class-entry check (an
8-entry per-request table) that the three methods are php-parser's own; an
overriding class, an uninitialized slot and getArgs() of a first-class
callable (whose assert() throws under zend.assertions=1) go through the
methods. MutatingScope (getKeepVoidType, expressionTypeIsUnchangeable, ...),
ClassStatementsGatherer, ClassReflection, MethodThrowPointHelper,
MethodCallReturnTypeHelper and VariableFlowBuilder switch to them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The DI service keeps the twin's constructor arginfo. A statement without a
doc comment - the common case - is answered from the scope's function and
the node's comments (pt_engine_node_get_comments(), which now leaves a
non-array `comments` attribute to the method's return-type TypeError, as
the twin does) without a call; with a doc comment the scope is asked through
the MutatingScope direct entries (getFile, isInClass, getClassReflection,
isInTrait, getTraitReflection, assignVariable) and FileTypeMapper,
ResolvedPhpDocBlock, VarTag, the function reflection and Comment::getText()
through cached method sites. Native callers use
pt_var_annotation_processor_process_var_annotation(); AssignHandler does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
… low

VariableLivenessResolver's collect() and liveBefore() recurse as deep as the
flow tree nests - a 10000-operand `$a + $a + ...` is a 10000-deep sequence -
where the twin recursed on the VM stack, so the native walk overflowed the C
stack (a segfault analysing such a function with the extension loaded).
They, and VariableFlowBuilder's targetRead(), targetWrite() and the writes()
recursion, now continue through pt_engine_with_stack() when the current
stack runs low.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
@ondrejmirtes
ondrejmirtes force-pushed the turbo-native-call-and-statement-handlers branch from dc1125f to 1b3a190 Compare September 22, 2026 11:06
@ondrejmirtes
ondrejmirtes merged commit 1b3a190 into 2.3.x Sep 22, 2026
@ondrejmirtes
ondrejmirtes deleted the turbo-native-call-and-statement-handlers branch September 22, 2026 11:06
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