Shadow the reflections, NodeCallbackScope and InitializerExprTypeResolver natively - #6535
Merged
Merged
Conversation
ondrejmirtes
force-pushed
the
turbo-native-reflections-and-initializers
branch
from
September 22, 2026 13:30
9102f8e to
041869a
Compare
…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
force-pushed
the
turbo-native-reflections-and-initializers
branch
from
September 22, 2026 13:43
041869a to
66f87da
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 parameter, variant and property reflections,
NodeCallbackScope,InitializerExprTypeResolverand 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
PhpParameterReflectionandExtendedNativeParameterReflection.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,ResolvedFunctionVariantWithOriginalandTrivialParametersAcceptor: the parameters acceptors every call site selects and resolves.AcceptorValues.hreads their slots and filled memos in place.Assertions, the@phpstan-asserttag collections.PhpPropertyReflection,ChangedTypePropertyReflectionandResolvedPropertyReflection: the property reflection chain every property fetch goes through.ResolvedPhpDocBlock,PhpVersionand php-parser'sName/Identifierhave 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 nativeMutatingScope. It answers rules' and collectors' questions from the stored expression results.InitializerExprTypeResolver, whole (48 methods), andInitializerExprContext. Native callers pass the twin'scallable $getTypeCallbackas 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,TemplateArgumentObserverandTemplateArgumentResolver. The PHPTemplateArgumentSolveronly runs for bodies with observations.RicherScopeGetTypeHelper,NullsafeOperatorHelperandLoopWrittenVariableNames.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:
ExpressionResultandMutatingScopereads,ClassReflection's member lookups, the parameter reflections' getters, and moreClassReflectionreaders.Build
make build-turbobuilds the extension in parallel over the local cores and reports the built binary's version. It warns when that version isn't the oneTurboExtensionEnableractivates, because such a build loads but stays inactive.🤖 Generated with Claude Code
https://claude.ai/code/session_017MvPby652L7wUqGAHEiEcN