Repository navigation
Shadow NodeScopeResolver and the narrowing core natively - #6521
Merged
Merged
Conversation
Ports both narrowing value classes over the twins' generated slots: the with*()/set*() clones, intersectWith()/unionWith() with the alternative-term algebra on the native TypeCombinator, and the context singletons kept in the twin's static $registry. emptySpecifyCallback() is one Closure over a NativeCallback holder cached in the twin's static slot, so invoking it from native code takes no frame. MutatingScope (applySpecifiedTypes, filterBy*Value) and ExpressionResult's branch scopes call the new pt_specified_types_* and pt_type_specifier_context_* entries; the typeSpecifierContext class-map key is removed. smoke.php compares both classes method by method. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
… natively ExpressionContext and StatementContext keep their state in the twins' promoted property slots; native callers get pt_expression_context_* and pt_statement_context_* direct entries (slots for a native context, the methods otherwise). ExprHandlerRegistry and StmtHandlerRegistry memoize in the twins' private static arrays with the exact cache keys (the CallLike first-class-callable and New_ class discriminators assembled in a stack buffer, so a hit allocates nothing) and export pt_expr_handler_registry_resolve() / pt_stmt_handler_registry_resolve(); MutatingScope's static call goes through the former. The exprHandlerRegistry class-map key is replaced by exprHandler and stmtHandler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…oundation Engine.h is what the analysis-engine ports build on: PHPStanTurbo\NativeClosure (a C++ body with its captures inline in one allocation, callable from PHP like the closure it replaces and entered without a frame by pt_type_call_callable and pt_direct_invoke), pt_expression_result_create() (the native constructor directly for the container-generated factory, whose extensions collection is learned from its first result; create() with the named arguments otherwise), handler entries dispatched by pt_expr_handler_process() / pt_stmt_handler_process(), and per-site method and property caches for the PHP collaborators not ported yet. ScalarHandler and VariableHandler use all of it plus the SpecifiedTypes / TypeSpecifierContext direct entries; composeResult() is exported as pt_variable_handler_compose_result(). New direct entries: ExpressionResult getters, four MutatingScope reads (the branch scopes' applySpecifiedTypes() now uses one), VariableFlow::mention() / all(READ_ALL). Class-map keys: impurePoint, issetabilityDescriptor. turbo-ext/CLAUDE.md documents the porting conventions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…setabilityDescriptor natively Ports the throw/impure point value classes, the argument walk result and the isset/empty/?? chain descriptor. Their slot getters are inline borrowed readers in the new AnalyserValues.h (the slot for the native class, the getter by name otherwise), which Engine.h includes: they replace the foundation's out-of-line ExpressionResult getters (getScope() / getThrowPoints() / getImpurePoints() / hasYield() / isAlwaysTerminating(), plus getBeforeScope() and getExpr()), and VariableFlowBuilder and VariableHandler read through them. VariableHandler creates impure points and descriptors through pt_impure_point_new() / pt_issetability_descriptor_variable() (the class-map keys impurePoint and issetabilityDescriptor are gone). Factories and behaviourful entries (toPublic(), resolve(), ...) are declared in support.h, plus MutatingScope entries for hasExpressionType() / getType() / getNativeType() next to the foundation's zend_string-keyed variable reads. Mirrors the twins' isset()/?? reads of never initialized properties and the read order of toPublic(). Differential: tests/analyser-values.php, run from smoke.php. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Ports InternalStatementResult, InternalStatementExitPoint and InternalEndStatementResult with the public StatementResult, StatementExitPoint and EndStatementResult toPublic() builds. The constructor joins the exit points' and end statements' inference facts through new MutatingScope direct entries (get/addTemplateArgumentConstraints, mergeWith for the loop back edge), reading them through inline slot readers added to AnalyserValues.h. The exit-point walks both result classes repeat are written once (StatementResults.h). Mirrors the "Undefined property" warning of a num read on a statement other than break/continue. Adds the class-map keys continueStmt and breakStmt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…Target natively Ports the template inference frame (isObserving(), returnTypeOfCall(), resolution through the parents, the unconstrained resolution with native callback holders for the traversal and the resolver) and the assignment target walk mode and prepared target. Their getters are inline slot readers in AnalyserValues.h. MutatingScope calls returnTypeOfCall() directly and gets direct entries for getCurrentTemplateArgumentFrame() and the nativeTypesPromoted property. Replaces the class-map key templateArgumentFrame with resolvedFunctionVariant; withTemplateArgumentFrame() checks its argument against the shadowing class or the class of that name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Ports the recording node callback of the convergence passes: PHP invokes the native __invoke() as a callable object through the engine, and pt_type_call_callable() - what the native invokers of node callbacks use - recognizes the class and records through pt_recording_node_callback_record() without a call. The differential invokes it every way PHP calls a callable object and through the native and PHP ClassStatementsGatherer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…rminatingCallHelper natively Ports the three call-handler helper services over the twins' generated slots, with the twins' exact constructor arginfo for autowiring. The scope and Type work runs natively through new MutatingScope direct entries (filterTypeWithMethod, getMethodReflection, getStateType, getConditionalExpressions, getCurrentExpressionResultStorage, resolveTypeByName, specifyTypesOfNewWorldHandlerNode); pt_mutating_scope_has_expression_type() now answers the PT_TRI_* value (IssetabilityDescriptor converted). InternalThrowPoint, ExpressionContext::isInThrow() and TemplateArgumentFrame go through their direct entries; the dynamic return/throw type extensions and the analyser classes still PHP are called by name. getAll() of a LazyExtensionsCollection is read from its memo slot (pt_extensions_collection_get_all). The twin's try/finally around the primed storage is mirrored with the engine's exception chaining. Class-map keys added: extensionClassHelper, lazyExtensionsCollection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Ports BooleanNarrowingHelper, ConditionalExpressionHolderHelper, ConditionalExpressionHolderRecipe, DisjunctionBranchUnionAugment and DisjunctionHolderProjectionAugment over the twins' generated slots. The compositions run on the native SpecifiedTypes/TypeSpecifierContext entries and construct the recipe/augments directly; MutatingScope::applySpecifiedTypes() evaluates recipes and the two augments through the new pt_conditional_expression_holder_recipe_evaluate() / pt_disjunction_*_augment_evaluate() entries, and the recipe reads its results through pt_expression_result_get_type_on_scope(). NodeScopeResolver and DefaultNarrowingHelper stay PHP (by name). Class-map keys added: booleanAndExpr, logicalAndExpr, booleanOrExpr, logicalOrExpr, parserIssetExpr. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Ports the narrowing service over the twin's generated slots with its exact constructor arginfo and @api signatures: create() and the null-safe unwrap run natively over the MutatingScope, Type, ExprPrinter and SpecifiedTypes entries (the chain tail calls of createNullsafeTypes() as a loop), the handler lookup through pt_expr_handler_registry_resolve(), and the per-class extension lookups merge natively over ExtensionClassHelper's static memo (pt_extension_class_helper_get_extension_class_names, also used by EarlyTerminatingCallHelper). MutatingScope::filterByTruthyValue()/filterByFalseyValue() call pt_type_specifier_specify_types_in_condition(). NullsafeOperatorHelper and the reflections stay PHP (by name). Class-map keys added: nullsafeOperatorHelper, coalesceExpr. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…memo ExpressionResult::consultExpressionTypeResolverExtensions() and MutatingScope::resolveType() asked the injected ExtensionsCollection for its list by name on every type resolution (~600K PHP frames per self-analysis); they now go through pt_extensions_collection_get_all(), which reads a LazyExtensionsCollection's $extensions memo slot once it is filled and calls getAll() only before that or for any other collection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Ports the helper whole: every public method is exported as a pt_default_narrowing_helper_* direct entry; VariableHandler's specifyTypesCallback and the two disjunction augments now call it through them. The twin's closures are native closures (the chain type reader handed out as a Closure, the issetability callbacks, both TypeTraverser callbacks of specifyTypesFromAsserts(), the template one keeping its by-reference capture); captureChainResults() walks its chain with an explicit stack in the twin's pre-order. New direct entries: ExpressionResult containsNullsafe/ getCreatedTypesForScope/getSpecifiedTypesForScope, MutatingScope toWalkScope, pt_expr_printer_print(), pt_specified_types_new_with_root_expr(), and the ImpurePoint getNode/isCertain slot readers in AnalyserValues.h. Class-map keys: typeExpr, identicalExpr. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
Ports the === / == / against-type narrowing whole, including the function families, the decided-comparison guard and specifyEqual()'s identical-type callback as a native closure. The public methods are exported as pt_identical_narrowing_helper_* direct entries; VariableHandler's captureFirstArgResult() and specifyIdentical() calls use them, and every DefaultNarrowingHelper call goes through its direct entries. The twin's receiver-then-argument evaluation of `$a->unionWith($b)` is sequenced explicitly. CountNarrowingHelper, RicherScopeGetTypeHelper and ClassReflection::asFinal() stay PHP behind cached method sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…onResult ExpressionResult's getTypeOnScope() / getSpecifiedTypesForScope() / getCreatedTypesForScope() and the other scope-flavoured reads looked the public property up by name through zend_read_property() on every call; they now take pt_mutating_scope_native_types_promoted(), which reads the slot of a MutatingScope and the property table of any other scope object with the same warning and uninitialized-read Error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3SmhnNkB3pEeVRTxqbdbu
…natively The walk hub runs natively: processExprNode/processStmtNode/processStmtNodesInternal, the on-demand walks and callNodeCallback reach handlers through pt_expr_handler_process/pt_stmt_handler_process and the native registries; StatementsHandler (with StatementListWalkState) walks statement lists, goto convergence and the two-pass body walk over the native statement results, TemplateArgumentFrame and RecordingNodeCallback; NonNullabilityHelper answers applyPendingEnsure from its slots. Direct entries: pt_node_scope_resolver_*, pt_statements_handler_*, pt_statement_list_walk_state_*, pt_non_nullability_helper_*, plus new MutatingScope, ExpressionResult, storage/stack and VariableFlow entries; the narrowing helpers' requireScopeStateType calls use the NodeScopeResolver entry. Engine.h adds pt_finally() (finally blocks run with the pending exception set aside), pt_engine_call_node_callback() (native closures, the native gatherer/recording callbacks and object callables without a by-name resolve), php-parser attribute reads, and pt_engine_with_stack(): native walk recursion continues on a fresh zend_fiber C stack when the stack runs low. NodeScopeResolver is not final: $this-calls take the native body only for exactly the native class. Its static guard properties stay real statics; MutatingScope reads them from the native class and calls processExprOnDemand through the direct entry. Class map: nodeScopeResolver dropped; keys added for the php-parser statement/expression classes, NodeFinder, NodeAbstract, the virtual nodes, PhpMethodReflection, NoopNodeCallback and the PHP template-argument/ensured-non-nullability classes the hub still calls. 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-engine-foundation
branch
from
September 22, 2026 09:59
ef6001c to
65a3b1a
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.
Makes the core of the analysis engine native:
NodeScopeResolverwithStatementsHandler, the handler registries, the narrowing machinery (TypeSpecifier,SpecifiedTypes,TypeSpecifierContextand the narrowing helpers), and the value classes a walk passes around. Apart from the#[ShadowedByTurboExtension]/#[ReferencedByTurboExtension]attributes, the PHP twins are unchanged.The engine-port foundation
Engine.his what the analysis-engine ports build on:PHPStanTurbo\NativeClosure: a C++ body with its captures inline in one allocation. PHP can call it like the closure it replaces, and native code enters it without a frame.pt_expression_result_create(): calls the native constructor directly for the container-generated factory.pt_expr_handler_process()/pt_stmt_handler_process().pt_finally(), which runs a finally block with the pending exception set aside.pt_engine_with_stack(): when the C stack runs low, native walk recursion continues on a freshzend_fiberstack.ScalarHandlerandVariableHandlerare the first two handlers on it.Value classes
SpecifiedTypesandTypeSpecifierContext, including the alternative-term algebra ofintersectWith()/unionWith().ExpressionContextandStatementContext.ImpurePoint,ThrowPoint,InternalThrowPoint,ArgsResultandIssetabilityDescriptor.StatementResult,StatementExitPointandEndStatementResultclasses.TemplateArgumentFrame,AssignTargetWalkModeandPreparedAssignTarget.RecordingNodeCallback.Getters are inline slot readers in
AnalyserValues.h: the slot for a native object, the getter by name for anything else.Services
ExprHandlerRegistryandStmtHandlerRegistry: they memoize with the twins' exact cache keys, and a hit allocates nothing.MethodCallReturnTypeHelper,MethodThrowPointHelperandEarlyTerminatingCallHelper.TypeSpecifier,DefaultNarrowingHelperandIdenticalNarrowingHelper. Each public method is exported as a direct entry, and the twins' closures are native closures.NodeScopeResolver,StatementsHandler(statement lists, goto convergence and the two-pass body walk) andNonNullabilityHelper.NodeScopeResolverisn't final, so$thiscalls take the native body only when the object is exactly the native class.Two collection reads, the expression type resolver extensions (~600K PHP frames per self-analysis) and the asking scope's
nativeTypesPromoted, now come straight from slots.Verification
The ported classes are compared against their twins by
smoke.php's differential tests and, for the engine as a whole, bywalk-trace.php, which records a fullNodeScopeResolverwalk over the default corpus with the native classes and with the PHP implementations and requires identical traces (612,019 lines). With the extension loaded and active,make tests,make phpstan,make cs,make lint,make lint-turboandmake sanitize-turboare clean, andmake pgowith 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