Skip to content

Shadow the remaining statement and expression handlers natively - #6533

Merged
ondrejmirtes merged 15 commits into
2.3.xfrom
turbo-native-remaining-handlers
Sep 22, 2026
Merged

ondrejmirtes merged 15 commits into
2.3.xfrom
turbo-native-remaining-handlers

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Shadows the remaining statement and expression handlers natively, together with the declaration processors and value classes they depend on. Apart from the #[ShadowedByTurboExtension] / #[ReferencedByTurboExtension] attributes, the PHP twins are unchanged.

Declarations

  • PhpDocsResolver, DeprecatedAttributeResolver, PropertyHooksProcessor and CalledMethodProcessor. getPhpDocs() answers with a native struct instead of an array.
  • PropertyHandler, ClassConstHandler, ConstHandler, EnumCaseHandler, TraitUseHandler and TraitHandler. TraitUseHandler's search for the used trait's node recurses natively under pt_engine_with_stack().
  • NamespaceHandler, UseHandler, GroupUseHandler, DeclareHandler, InlineHtmlHandler, GlobalHandler, StaticVariableHandler and UnsetHandler. UnsetHandler builds its ExistingArrayDimFetch chain with a native recursion instead of the twin's recursive closure.

Expressions

  • Casts, unary operators and string interpolation, with ImplicitToStringCallHelper. The cast handlers inline getCastType()'s dispatch.
  • Increment and decrement, with IncDecTypeHelper. Constant scalar values are stepped with the engine's own increment/decrement functions.
  • clone, eval, exit, include, print, shell exec, throw, the @ error-suppress operator and the pipe operator.
  • yield and yield from, with CountNarrowingHelper.
  • VirtualExprResultHelper and nine virtual expression handlers.
  • The four first-class-callable node handlers.
  • instanceof and array literals. An array literal's next implicit index stays a zval, so $nextIndex++ overflows to float exactly as in the twin.
  • isset, empty and nullsafe method calls, with the issetability value classes (IssetabilityResolution, IssetabilityLinkInfo, EnsuredNonNullabilityResult(Expression)).
  • match: the enum-case fast path, the per-arm verdicts and narrowings, exhaustiveness, and the UnhandledMatchError throw point.

Type, specify-types and create-types callbacks are native closures, and each handler is registered as a handler entry.

Coverage

The walk-trace corpus gains fixtures for array literals with implicit keys around PHP_INT_MAX, where the next implicit key becomes unpredictable in both the twin and the port.

🤖 Generated with Claude Code

https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN

ondrejmirtes and others added 15 commits September 22, 2026 14:41
…cessor and CalledMethodProcessor natively

Ports the declaration processors. getPhpDocs() / getDeprecatedAttribute() are exported as
pt_php_docs_resolver_get_php_docs() (a pt_php_docs struct, no array; the list() warning
semantics kept for a PHP resolver) and pt_deprecated_attribute_resolver_get_deprecated_attribute();
processPropertyHooks(), processCalledMethod() and clearCalledMethodResults() as
pt_property_hooks_processor_* / pt_called_method_processor_*. ClassMethodHandler,
FunctionHandler, ClassLikeHandler, MethodCallHandler and PhpClassReflectionExtension now call
them directly. transformStaticType()'s traverser callback and the hook / called-method node
callbacks are native closures; the php-parser getters (getDocComment()->getText(),
getParams(), getReturnType(), getStmts()) read slots while the vendored method is inherited.
The InitializerExprContext of getDeprecatedAttribute() is built on first use (pure factory).
New entries: ClassReflection isImmutable/acceptsNamedArguments/isAnonymous/getResolvedPhpDoc,
MutatingScope getParentScope/enterTrait/enterNamespace/enterDeclareStrictTypes/
enterPropertyHook, NodeScopeResolver isAnalysedFile, pt_node_doc_comment_text(); NodeTraverser's
class entry is extern. Class-map keys added: propertyHook, inPropertyHookNode,
propertyHookReturnStatementsNode, lineAttributesVisitor. Twin bugs mirrored in
PropertyHooksProcessor: property-assign impure points of hooks go to the unused
$hookImpurePoints, and a hook without statements ends processing of the later hooks.

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

Ports the declaration statement handlers as registered statement-handler entries. They
call AttributesHandler, PhpDocsResolver (the pt_php_docs struct), PropertyHooksProcessor,
MutatingScope, ClassReflection, the reflection provider, ExpressionResultStorage and
NodeScopeResolver through their direct entries. PropertyHandler's per-item statement clone
takes the item's attributes through NodeAbstract's slot while getAttributes() /
setAttributes() are inherited. TraitUseHandler's search for the used trait's node recurses
natively under pt_engine_with_stack() and applies the alias adaptations to method clones
like the twin. Class-map keys added: traitUseStmt, enumCaseStmt, traitUseAdaptationAlias,
inTraitNode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
… InlineHtmlHandler, GlobalHandler, StaticVariableHandler and UnsetHandler natively

Ports the remaining statement handlers as registered statement-handler entries.
GlobalHandler and StaticVariableHandler call VarAnnotationProcessor, VariableFlow(Builder),
StaticTypeFactory and MutatingScope directly; UnsetHandler calls AssignHandler's virtual
assign, MethodThrowPointHelper, VariableWriteOffset, the storage and NodeScopeResolver
directly and builds the ExistingArrayDimFetch chain with a native recursion instead of the
twin's recursive closure (the container and VirtualExprResultHelper stay cached sites).
New entry: pt_variable_flow_discard(). Class-map keys added: useStmt, groupUseStmt,
unsetOffsetExpr (foreachValueByRefExpr reused from ForeachHandler).

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

CastHandler, CastStringHandler, InterpolatedStringHandler, UnaryMinusHandler,
UnaryPlusHandler and BitwiseNotHandler register their processExpr() bodies as
handler entries; their type, operand-reader and specify-types callbacks are
native closures. The cast handlers spell out InitializerExprTypeResolver's
getCastType() dispatch (no per-call operand closure); the unary handlers keep
calling the resolver (it consults the unary operator extensions).
ImplicitToStringCallHelper is exported as
pt_implicit_to_string_call_helper_process_implicit_to_string_call(), which
BinaryOpHandler, EchoHandler and AssignOpHandler now call directly. New
entry pt_mutating_scope_obtain_result_for_node(); shared bits in
SimpleExprHandlers.h. Class-map keys for the Cast subclasses, UnaryPlus,
BitwiseNot and InterpolatedStringPart. No twin bugs found.

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

PreIncHandler, PreDecHandler, PostIncHandler and PostDecHandler register their
processExpr() bodies as handler entries and call AssignHandler's virtual
assignment and NodeScopeResolver's result store through their direct entries.
IncDecTypeHelper::getTypeCallback() is exported as
pt_inc_dec_type_helper_get_type_callback(), returning the native closure holder
(the PHP method wraps it in the \Closure its return type demands); its
constant-scalar arm steps values with the engine's increment_function() /
decrement_function() and calls str_increment() / str_decrement() through the
function table, catching their ValueError like the twin. getPlusType() /
getMinusType() stay cached sites. No twin bugs found.

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

CloneHandler, EvalHandler, ExitHandler, IncludeHandler, PrintHandler,
ShellExecHandler, ThrowHandler, ErrorSuppressHandler and PipeHandler register
their processExpr() bodies as handler entries; their type and specify-types
callbacks are native closures. CloneHandler::resolveCloneType() is exported as
pt_clone_handler_resolve_clone_type() (FuncCallHandler now calls it directly;
the cloneHandler class-map key is gone); the CloneTypeTraverser it hands
TypeTraverser::map() is a native closure over the traverser's body.
PipeHandler reads the nodes' attributes through cached getAttributes() /
getAttribute() sites. Class-map keys exitExpr, printExpr, shellExecExpr and
errorSuppressExpr. No twin bugs found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
YieldHandler and YieldFromHandler register their processExpr() bodies as
handler entries; their type callbacks (the enclosing function's TSend / the
operand's TReturn) and specify-types callbacks are native closures.
CountNarrowingHelper is exported as
pt_count_narrowing_helper_is_normal_count_call() (the PT_TRI_* value) and
pt_count_narrowing_helper_specify_count_size(), which BinaryOpHandler and
IdenticalNarrowingHelper now call directly. Twin quirk mirrored: YieldHandler
replaces the implicit throw point with a yielded key's throw points instead of
merging them.

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

VirtualExprResultHelper, TypeExprHandler, NativeTypeExprHandler,
UnsetOffsetExprHandler, AlwaysRememberedExprHandler,
ExistingArrayDimFetchHandler, IssetExprHandler, PossiblyImpureCallExprHandler,
SetExistingOffsetValueTypeExprHandler and SetOffsetValueTypeExprHandler run
natively: processExpr() bodies registered as handler entries, their type /
specify / create-types closures as native closures over the twins' captures,
the sub-walks through NodeScopeResolver's direct entry. New entries
pt_virtual_expr_result_helper_create_type_expr_result() /
_create_unset_offset_expr_result(); AssignHandler's createTypeExprResult() and
UnsetHandler's createUnsetOffsetExprResult() sites switched to them. The
virtual nodes stay PHP (final classes read in their private slots; ~13K
constructions per ExprHandler-sized slice made porting them pointless). No
twin bugs mirrored.

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

The four first-class-callable virtual handlers run natively: processExpr()
bodies registered as handler entries, the dynamic name / class / receiver
walks through NodeScopeResolver's direct entry with ExpressionContext's
createDeep(), their type and specify closures as native closures over the
twins' captures, the private resolveType() bodies of the function and
method variants reading the captured results through ExpressionResult's and
MutatingScope's direct entries. InitializerExprTypeResolver and
InitializerExprContext stay PHP behind cached method sites. Mirrored twin
quirk: MethodCallableNodeHandler ignores the receiver's isAlwaysTerminating()
(only a dynamic name's counts), unlike StaticMethodCallableNodeHandler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The instanceof and array literal handlers with their type and specify-types
callbacks as native closures (ArrayHandler's item-type callback handed to
InitializerExprTypeResolver::getArrayType(), which stays PHP behind a cached
site), registered as handler entries. The array literal's value-flow target
reads the VariableWrite slots; its $nextIndex is kept as a zval so the
int-overflow-to-float of `$nextIndex++` / max() stays the twin's.
Class-map keys literalArrayItem and literalArrayNode.

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

The isset / empty / ?? chain value classes: IssetabilityLinkInfo's factories and
getters and IssetabilityResolution's isSet() / notEmpty() folds (a native inner
resolution and the link's facts read in place, notEmpty()'s callback a native
closure), and the ensured-non-nullability carriers NonNullabilityHelper returns.
New entries pt_issetability_link_info_variable / _offset / _property / _leaf /
_is_kind, pt_issetability_resolution_new / _is_set / _not_empty,
pt_ensured_non_nullability_result_new / _expression_new and their AnalyserValues.h
readers; IssetabilityDescriptor, ExpressionResult, DefaultNarrowingHelper,
NonNullabilityHelper, NullsafePropertyFetchHandler, CoalesceHandler and
CoalesceCompositionHelper switched to them. Class-map
keys removed: issetabilityResolution, issetabilityLinkInfo,
ensuredNonNullabilityResult, ensuredNonNullabilityResultExpression. Differential
coverage in analyser-values.php.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The isset / empty / ?-> call handlers with their type, specify-types and
create-types callbacks as native closures, registered as handler entries:
isset()'s multi-subject conjunction fold (the accumulated closure memoized
through a shared reference capture), empty()'s `!isset($x) || !$x` disjunction
with its memoized branch scopes, and the nullsafe call's plain-twin walk with the
short-circuit scope merge. The issetability verdicts go through
pt_issetability_resolution_is_set / _not_empty, the ensured scopes through the
EnsuredNonNullabilityResult readers. Class-map keys issetExpressionNode,
emptyExpressionNode, nullsafeMethodCallExpressionNode and emptyExpr (booleanNotExpr
shared with BooleanNotHandler).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The match expression handler: the enum-case fast path, the per-arm `subject ===
cond` verdicts and narrowings (the identical-verdict callback handed to
IdenticalNarrowingHelper::specifyIdentical() a native closure, the arm-condition
specify closure inlined), the in_array() filtering of multi-condition arms, the
exhaustiveness and UnhandledMatchError throw point, the arm variable flows and the
captured arm results in the twin's own array slot, registered as the handler entry.
AssignHandler's getCapturedArmScopesAndTypes() site uses the new
pt_match_handler_get_captured_arm_scopes_and_types entry. New entries
pt_mutating_scope_enter_match / _add_type_to_expression / _remove_type_from_expression;
the exhaustiveness check reuses pt_expression_result_answers_on_scope.
RicherScopeGetTypeHelper, getStartLine() and AlwaysRememberedExpr::getExpr() stay
PHP behind cached sites. Class-map keys matchExpressionArm, matchExpressionArmBody,
matchExpressionArmCondition, matchExpressionNode and arrayItem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
processArrayByRefItems() computes the key each item of an assigned array
literal gets: an explicit PHP_INT_MAX key, a later key compared against it
and the implicit increment past it all end with the next implicit key being
unpredictable (advanceImplicitIndex()), in the twin and the native port. The
cases are walk-trace fixtures in turbo-ext/tests/walk-trace-fixtures (the
default corpus).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
@ondrejmirtes
ondrejmirtes merged commit 456ca7e into 2.3.x Sep 22, 2026
580 of 584 checks passed
@ondrejmirtes
ondrejmirtes deleted the turbo-native-remaining-handlers branch September 22, 2026 12:55
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