Skip to content

Shadow the reflections, NodeCallbackScope and InitializerExprTypeResolver natively - #6535

Merged
ondrejmirtes merged 17 commits into
2.3.xfrom
turbo-native-reflections-and-initializers
Sep 22, 2026
Merged

ondrejmirtes merged 17 commits into
2.3.xfrom
turbo-native-reflections-and-initializers

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Shadows the parameter, variant and property reflections, NodeCallbackScope, InitializerExprTypeResolver and the template argument classes natively, and replaces the remaining by-name calls between native classes with direct calls. Apart from the #[ShadowedByTurboExtension] / #[ReferencedByTurboExtension] attributes, the PHP twins are unchanged.

Reflections and acceptors

  • PhpParameterReflection and ExtendedNativeParameterReflection. pt_parameter_reflection_call() dispatches any parameter reflection: it reads the slots of the known classes in place, runs the native bodies, and otherwise uses one cached method site per member.
  • FunctionVariant, ExtendedFunctionVariant, ExtendedCallableFunctionVariant, ResolvedFunctionVariantWithOriginal and TrivialParametersAcceptor: the parameters acceptors every call site selects and resolves. AcceptorValues.h reads their slots and filled memos in place.
  • Assertions, the @phpstan-assert tag collections.
  • PhpPropertyReflection, ChangedTypePropertyReflection and ResolvedPropertyReflection: the property reflection chain every property fetch goes through.
  • Native readers for answers the vendored BetterReflection adapters, ResolvedPhpDocBlock, PhpVersion and php-parser's Name/Identifier have already memoized. They are guarded by exact class checks; any other class, or an answer not yet filled, still calls the method.

Engine

  • NodeCallbackScope, the final subclass of the native MutatingScope. It answers rules' and collectors' questions from the stored expression results.
  • InitializerExprTypeResolver, whole (48 methods), and InitializerExprContext. Native callers pass the twin's callable $getTypeCallback as a function pointer plus stack data. When a PHP collaborator keeps that callable after the call returns, it gets a closure that owns copies of what it reads instead of pointing into a dead stack frame; ASan found this as a stack-use-after-return.
  • TemplateArgumentConstraints, TemplateArgumentObserver and TemplateArgumentResolver. The PHP TemplateArgumentSolver only runs for bodies with observations.
  • RicherScopeGetTypeHelper, NullsafeOperatorHelper and LoopWrittenVariableNames.

Direct calls

A census of the engine calls made from native code, taken by the C return address, located the by-name calls between two native classes. They now use direct entries and new Type ops: ExpressionResult and MutatingScope reads, ClassReflection's member lookups, the parameter reflections' getters, and more ClassReflection readers.

Build

make build-turbo builds the extension in parallel over the local cores and reports the built binary's version. It warns when that version isn't the one TurboExtensionEnabler activates, because such a build loads but stays inactive.

🤖 Generated with Claude Code

https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN

@ondrejmirtes
ondrejmirtes force-pushed the turbo-native-reflections-and-initializers branch from 9102f8e to 041869a Compare September 22, 2026 13:30
ondrejmirtes and others added 17 commits September 22, 2026 15:43
…atively

The parameter reflections of userland and built-in functions. PhpParameterReflection
keeps its $type / $nativeType memo slots and asks the BetterReflection adapter,
InitializerExprTypeResolver and InitializerExprContext through cached sites.
pt_parameter_reflection_call() / _bool() (ParameterValues.h) is the dispatch for
any ParameterReflection: slot reads of Dummy/ExtendedDummy/Native/ExtendedNative
parameter instances in place, the native bodies, one cached site per member
otherwise. ArgumentsHandler, ArgumentsNormalizer, ParametersAcceptorSelector,
SimpleImpurePoint, the closure resolvers, DefaultNarrowingHelper and the member
prototypes use it. Class-map key extendedNativeParameterReflection is gone;
pt_extended_native_parameter_reflection_new() replaces it.

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

The parameters acceptors every call site selects and resolves. The variants keep the
twins' slots (the non-final @api ones matched by exact class entry); the resolved
variant keeps its memo slots and WeakReference memo, its getParameters() mapping and
TypeTraverser callbacks are native closures. pt_parameters_acceptor_call() / _read() /
_bool() (AcceptorValues.h) read slots and filled memos in place and reach the native
bodies or one cached site per member otherwise; the call handlers' helpers, ArgumentsHandler,
ArgumentsNormalizer, ParametersAcceptorSelector, DefaultNarrowingHelper, SimpleImpurePoint,
TemplateArgumentFrame and the member prototypes use it. New entries: pt_*_variant_new(),
pt_resolved_function_variant_with_original_new(), pt_trivial_parameters_acceptor_new(),
pt_resolved_function_variant_get_return_type_with_unresolved_template_arguments().
Class-map keys functionVariant, extendedFunctionVariant, extendedCallableFunctionVariant,
resolvedFunctionVariantWithOriginal and trivialParametersAcceptor are gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The @phpstan-assert tag collections: the private static $empty singleton, the
filters' and mapTypes()' array_filter() / array_map() closures as native loops
with their AssertTag parameter checks, union() / intersect() and the factories;
AssertTag and AssertTagParameter stay PHP behind cached sites. Entries:
pt_assertions_create_empty(), pt_assertions_create_from_resolved_php_doc_block()
(the class under the twin's real name, so a prefixed differential hands PHP
collaborators the twin), pt_assertions_map_types(), pt_assertions_get_asserts();
pt_assertions_all() moved here. The call handlers, the method reflections and
prototypes, the callable types, PhpDocsResolver, DefaultNarrowingHelper and the
variants use them. Class-map key assertions is gone; assertTag and
resolvedPhpDocBlock are new.

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

A call-site census (C return addresses of every engine call made from native
code) located the by-name calls between two native classes; they now take
direct entries: ExpressionResult's scope reads and late-resolvable resolution,
MutatingScope's stored-result asks, other-scope getters, template constraints
and VolatileExpressionHelper bodies, ClassReflection's PhpClassReflectionExtension
member lookups (new pt_php_class_reflection_extension_* entries), and
ExtendedDummyParameter's impurity slot. New Type ops (getMethod,
getInstanceProperty, the unresolved prototypes' getTransformed*/getNaked*,
ClassReflection's hot readers) make the remaining by-name callers direct too.
PT_TYPE_METHOD_IS / PT_TYPE_METHOD_INHERITED memoize the subclass test per call
site, so MutatingScope's direct entries take the native body for inherited
methods of a subclass without a function-table lookup per call.
Self-analysis N>N engine calls 20.6M -> 7.0M.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The native NodeCallbackScope (final, extending the native MutatingScope)
answers rule and collector asks from the stored expression results: per-node
memo, settled stored results guarded by the scope's position, the filters
replayed onto before scopes, the walk-flavour scope seeded or built once. It
reaches the inherited bodies through new pt_mutating_scope_* entries and is
reached through pt_node_callback_scope_* ones, which the dispatching
MutatingScope entries and $this-calls now take for that class.
LazyInternalScopeFactory's toNodeCallbackScopeFactory()/toWalkScopeFactory()
are read out of its $twin/$origin slots. The nodeCallbackScope class-map key
is gone; walk-trace.php also records getKeepVoidType(), filtered scopes'
types and push/popInFunctionCall() on the callback scope.

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

The call-site census's next tail: Type ops for the parameter reflections'
getters (DummyParameter, NativeParameterReflection,
ExtendedNativeParameterReflection, PhpParameterReflection), the member
reflections' isPublic() (ResolvedMethodReflection), the unresolved
prototypes' doNotResolveTemplateTypeMapToBounds() and more ClassReflection
readers (getName, getFileName, getParentClass, getObjectType,
getTemplateTypeMap, getResolvedPhpDoc, getAncestorWithClassName, withTypes),
so every by-name caller of those takes the C++ body; TemplateTypeVariance
compose()/invariant()/bivariant() through its entries, and MutatingScope's
IsSuperTypeOfResult::no(), getIterable*Type() and member-reflection reads of
other scopes through the dispatching entries. Self-analysis N>N -1.1M.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Ports PHPStan\Reflection\InitializerExprTypeResolver whole (48 methods): getType()'s
constant-expression walk (arms memoized per node class), the arithmetic / bitwise /
comparison / concat / unary / cast type methods on the engine's operator functions
and internal min() / max(), array literals, function types, first-class callables
and class constant fetches with the twin's memo arrays. Native callers now use the
pt_initializer_expr_type_resolver_* entries and hand the twin's `callable
$getTypeCallback` as a pt_ietr_get_type (function pointer + stack data) instead of
allocating a NativeClosure and crossing into PHP twice; a PHP collaborator needing a
callable (OversizedArrayBuilder) gets a NativeClosure over it. CALCULATE_SCALARS_LIMIT
is a native constant, class-map key initializerExprTypeResolver is gone; magicConst*
and templateTag are new. type-family.php observes ~28K results over a new fixture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
A stack-backed pt_ietr_get_type handed to a PHP collaborator that keeps the
callable (OversizedArrayBuilder, a resolver that is not the native class) was
wrapped in a NativeClosure holding the callback's address, so calling the kept
callable after the call returned read a dead stack frame (ASan:
stack-use-after-return in bridgeGetTypeBody). pt_ietr_get_type now carries a
required toCallable(data): every caller builds its NativeClosure over copies of
the values its frame points at (the closure bodies the handlers had before the
port, restored), the resolver its own over $this and $context, a PHP callable is
passed on itself. tests/initializer-escape.php keeps the callables of seven
callers and calls them after the call; walk-trace gains an oversized-array
fixture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
The seven slots behind the private constructor; fromScope() over MutatingScope's
direct entries, the function reflection and BetterReflection adapters behind
cached sites, Identifier/Name toString() read from $name while inherited.
Entries pt_initializer_expr_context_from_*() / _create_empty() (the PHP twin
under the prefixed activation) and inline slot readers in AnalyserValues.h; the
handlers, MutatingScope, ClassReflection, PhpClassReflectionExtension,
PhpParameterReflection, DeprecatedAttributeResolver and InitializerExprTypeResolver
use them. Class-map key initializerExprContext is gone; scope,
adapterReflectionParameter, adapterReflectionFunction and betterReflectionConstant are new.

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

The fact trees (getFacts() materialized in the generator's order), the
observer's sites, sends and lower bounds with its traversal closures as native
callback holders, and the resolver walking the facts in place; the PHP
TemplateArgumentSolver (class-map key templateArgumentSolver) runs only for
bodies with observations — solve() over none is [] — and Stats stays PHP.
Entries pt_template_argument_constraints_*(), _facts(), pt_template_argument_observer_collect_*()
and pt_template_argument_resolver_resolve(); the walk hub, the statement,
assign and argument handlers and MutatingScope use them. Class-map key
templateArgumentConstraints is gone.

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

The `===` / `!==` pricing (getNotIdenticalResult() prices the NotIdentical's
operands without the throwaway Identical node; resolveIdenticalType() through
the resolver's direct entry), the nullsafe chain rewrite with its attribute
memo, and the loop-written names over memoized subnode offsets and the native
VariableFlow slots; PropertyReflectionFinder and rebuilt php-parser nodes stay
PHP. Entries pt_richer_scope_get_type_helper_get_(not_)identical_result(),
pt_nullsafe_operator_helper_*() and pt_loop_written_variable_names_collect();
class-map keys nullsafeOperatorHelper and loopWrittenVariableNames are gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
BetterReflectionAccess.cpp answers the vendored Adapter\ReflectionClass /
ReflectionEnum / ReflectionMethod / ReflectionProperty / ReflectionNamedType /
ReflectionParameter getters from the wrapped BetterReflection objects'
immutable properties and filled memos (cachedName, cachedTraits,
cachedInterfaceNames, cachedInterfaces, cachedConstructor with the
isConstructor() scan over cachedMethods, cachedConstants, locatedSource by the
class declaring isInternal()), guarded by exact adapter and wrapped classes; an
unfilled memo calls the wrapped method, anything else the adapter's.
ClassReflection, PhpClassReflectionExtension (whose adapter memo slots now come
from the shared readers), TypehintHelper, ObjectType, ClassLikeHandler,
CalledMethodProcessor, TraitUseHandler and InitializerExprContext's
fromReflectionParameter() use pt_class_adapter_* /
pt_method_adapter_* / pt_property_adapter_* / pt_named_type_adapter_* /
pt_better_reflection_*; the trait walks read names only, which the twin's
never-matching in_array() identity test makes exact. New class-map keys for the
BetterReflection method, property, named type, parameter, class constant and
InternalLocatedSource classes and the property adapter.

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

The property reflection chain every property fetch goes through is native:
PhpPropertyReflection (its adapter getters through the BetterReflection
readers, TypehintHelper::decideType() for the readable/writable memos),
ChangedTypePropertyReflection and ResolvedPropertyReflection (template
resolution memoized as the twin does), each with the getName() / isPublic() /
isFinal() / isFinalByKeyword() Type ops. pt_extended_property_reflection_call()
/ _bool / _has_hook / _get_hook dispatch any property reflection (PT_PROP_*),
pt_class_member_reflection_call() any class member; PhpMethodReflection stays
PHP and pt_extended_method_reflection_call() answers its getters from its
slots, $name/$variants/$returnType memos and adapter
(PhpMethodReflectionAccess.cpp). The prototypes, MutatingScope's visibility
checks, ObjectType/StaticType, ClassReflection and several handlers use the
dispatches; class-map keys of the three shadowed classes are gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
ResolvedPhpDocBlockAccess.cpp answers the resolved PHPDoc block's getters from
their private slots once resolved — each memo's own "not yet" sentinel (false
for the tag lists and the self-out tag, a bool for the return / throws /
deprecated tags, null for the ??= flags, 'notLoaded' for isPure()) — and from
the constructor-written slots of the plain getters; an unresolved memo, an
uninitialized slot or any other class calls the getter. PhpDocsResolver,
ClassReflection, PhpClassReflectionExtension, StatementsHandler and
VarAnnotationProcessor use pt_resolved_php_doc_block_call() / _bool(), keyed
by the existing resolvedPhpDocBlock class-map entry.

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

PhpVersionAccess.cpp answers the PhpVersion queries the engine asks
(supportsLegacyConstructor(), supportsReadOnlyProperties(), ...) from the
$versionId slot of exactly the final PhpVersion. NameNodeAccess.cpp reads
toString() / toLowerString() / __toString() / isFullyQualified() from the
`name` slot of exactly Name, FullyQualified, Relative, Identifier and
VarLikeIdentifier, and constructs them with a non-empty string in place.
Any other class, or an uninitialized slot, still calls the method.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
`make build-turbo` runs the turbo-ext build in parallel over the local
cores (plain `make -C turbo-ext` compiles the ~370 objects one at a time)
and then reports the built binary's baked version, warning when it is not
the one TurboExtensionEnabler activates — a build from a shallow clone or
one made between a turbo-ext/src commit and its `make bump-turbo` loads
but stays inactive, which otherwise only shows up as "nothing got faster".

The version probe runs ini-less: where the machine's php.ini already loads
the extension, the "Module is already loaded" warning a second -d prints
goes to stdout, into the version being read.

The two turbo e2e jobs call the target instead of spelling out the build.
phar.yml keeps its own `make pgo` step: it is a profile-guided build with
the strict -Werror set that local builds deliberately stay lenient about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzFKEPN1Vrpj4GdDXXbWzr
@ondrejmirtes
ondrejmirtes force-pushed the turbo-native-reflections-and-initializers branch from 041869a to 66f87da Compare September 22, 2026 13:43
@ondrejmirtes
ondrejmirtes merged commit 66f87da into 2.3.x Sep 22, 2026
1 check passed
@ondrejmirtes
ondrejmirtes deleted the turbo-native-reflections-and-initializers branch September 22, 2026 13:43
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