Skip to content
Prev Previous commit
Next Next commit
Dispatch hot Type operations between native classes directly
A native body calling another native object's equals()/isSuperTypeOf()/
traverse()/... went through the engine (pt_type_call: method lookup,
zend_call_function frame, argument copies, the handler's zpp) — 34.2M
such calls per self-analysis, each landing in a one-line delegation.

- TypeOps.h/.cpp: a pt_type_op_id enum of 27 hot operations with their
  argument contracts, a per-class table of direct C++ entries (`zv::Val
  (*)(zend_object *self, zend_class_entry *scope, uint32_t argc, zval
  *argv)`), and pt_type_op()/pt_type_op_trinary(): the entry is called
  when the receiver is EXACTLY a registered native class that registered
  the op and the arguments satisfy the contract; anything else (a PHP
  subclass, a reference, a wrong zval kind) takes pt_type_call() as
  before. The ops of a class entry are found in a direct-mapped
  open-addressing table keyed by the zend_class_entry pointer (the engine
  leaves no CE field free for a runtime-linked user class), filled at
  activation in Shadow.cpp; a native subclass inherits the parent's entry
  (with the parent's scope) for every method it does not declare itself.
- reg::Class::op()/traitOp() register the entries next to the
  cls.method()/cls.traitMethod() lines they mirror, an entry that only
  delegates to a handle member is generated from it (cls.op<PT_OP_X,
  &Handle::member>(): reg::detail::BoundOp derives the argv conversions
  from the member's parameter types and the result conversion from its
  return type; a member may ignore trailing op arguments but never take
  more than the op passes), any other is a one-line lambda; a type op read
  as a bool is pt_type_op_bool(); traitOp() binds to the immediately
  preceding traitMethod() and only when that call added the method, so the
  class-body/first-trait precedence is kept.
- The callback holders (PHPStanTurbo\NativeCallback, ObjectTypeCallback)
  and TypeTraverser::mapInternal()/traverseInternal() are entered without
  a frame by pt_call_fci() and pt_type_call_callable() when a resolved
  callable is one of them (pt_direct_invoke); their PHP-visible __invoke()
  and methods stay.
- pt_type_call() resolves the name to an op (length-bucketed memcmp, only
  for a receiver with an ops table), so the per-file by-name wrappers take
  the direct path too; 467 literal call sites use pt_type_op() outright;
  pt_call_type_equals()/pt_type_describe_precise() and ScopeOps' type
  queries likewise; the multi-statement JustNullable/ConstantScalar trait
  handlers share their bodies with the entries.

Skipped on the direct path, and why that is safe: the execute_data frame
(no observer/backtrace entry — pure queries), the handler's zpp (replaced
by the op's argc/zval-kind pre-check with engine fallback; zpp checks no
class either), the frame's argument/receiver addrefs (borrowed from the
caller for the call — immutable Type/result objects), and the internal
return-type check (never done at run time in release builds).

Call census (self-analysis of src/Analyser, src/Rules, src/Type): native ->
native calls through the engine 34.2M -> 7.2M; user CPU -2.3% over three
interleaved pairs against the base extension; analysis output identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MShHKdUB19w38vboJLPKXy
  • Loading branch information
ondrejmirtes and claude committed Sep 22, 2026
commit fe6293f27d23281c62d1897b68c67b2d74eea633
2 changes: 1 addition & 1 deletion turbo-ext/config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if (PHP_PHPSTAN_TURBO != "no") {
// directory by splitting on backslashes only — with forward slashes the
// objects compile flat while the link list expects the subpath
EXTENSION("phpstan_turbo",
"src\\main.cpp src\\support.cpp src\\AbsorbedTemplateArgumentType.cpp src\\AcceptsResult.cpp src\\AccessoryArrayListType.cpp src\\AccessoryDecimalIntegerStringType.cpp src\\AccessoryLiteralStringType.cpp src\\AccessoryLowercaseStringType.cpp src\\AccessoryNonEmptyStringType.cpp src\\AccessoryNonFalsyStringType.cpp src\\AccessoryNumericStringType.cpp src\\AccessoryUppercaseStringType.cpp src\\ArenaCache.cpp src\\ArrayType.cpp src\\BenevolentUnionType.cpp src\\BooleanType.cpp src\\CallableType.cpp src\\CallableTypeHelper.cpp src\\CircularTypeAliasErrorType.cpp src\\ClassConstantAccessType.cpp src\\ClassReflectionAccess.cpp src\\ClassStringType.cpp src\\ClosureType.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ConditionalType.cpp src\\ConditionalTypeForParameter.cpp src\\ConstantArrayType.cpp src\\ConstantArrayTypeBuilder.cpp src\\ConstantBooleanType.cpp src\\ConstantFloatType.cpp src\\ConstantIntegerType.cpp src\\ConstantStringType.cpp src\\ConstantTypeHelper.cpp src\\EnumCaseObjectType.cpp src\\ErrorType.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\FiniteTypeSet.cpp src\\FloatType.cpp src\\GenericClassStringType.cpp src\\GenericObjectType.cpp src\\GenericStaticType.cpp src\\GetTemplateTypeType.cpp src\\HasMethodType.cpp src\\HasOffsetType.cpp src\\HasOffsetValueType.cpp src\\HasPropertyType.cpp src\\IntegerRangeType.cpp src\\IntegerType.cpp src\\IntersectionType.cpp src\\IsSuperTypeOfResult.cpp src\\IterableType.cpp src\\KeyOfType.cpp src\\LateResolvableArrayShapeType.cpp src\\LruCache.cpp src\\MixedType.cpp src\\NeverType.cpp src\\NewObjectType.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\NonAcceptingNeverType.cpp src\\NonEmptyArrayType.cpp src\\NonexistentParentClassType.cpp src\\NullType.cpp src\\ObjectShapeType.cpp src\\ObjectType.cpp src\\ObjectWithoutClassType.cpp src\\OffsetAccessType.cpp src\\OversizedArrayType.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\RecursionGuard.cpp src\\ResourceType.cpp src\\ScopeContext.cpp src\\ScopeOps.cpp src\\Shadow.cpp src\\StaticType.cpp src\\StaticTypeFactory.cpp src\\StrictMixedType.cpp src\\StringAlwaysAcceptingObjectWithToStringType.cpp src\\StringNeverAcceptingObjectWithToStringType.cpp src\\StringType.cpp src\\SymbolFinderInFiles.cpp src\\TemplateArrayType.cpp src\\TemplateBenevolentUnionType.cpp src\\TemplateBooleanType.cpp src\\TemplateConstantArrayType.cpp src\\TemplateConstantIntegerType.cpp src\\TemplateConstantStringType.cpp src\\TemplateFloatType.cpp src\\TemplateGenericObjectType.cpp src\\TemplateIntegerType.cpp src\\TemplateIntersectionType.cpp src\\TemplateIterableType.cpp src\\TemplateKeyOfType.cpp src\\TemplateMixedType.cpp src\\TemplateNullType.cpp src\\TemplateObjectShapeType.cpp src\\TemplateObjectType.cpp src\\TemplateObjectWithoutClassType.cpp src\\TemplateStrictMixedType.cpp src\\TemplateStringType.cpp src\\TemplateTypeArgumentStrategy.cpp src\\TemplateTypeFactory.cpp src\\TemplateTypeHelper.cpp src\\TemplateTypeMap.cpp src\\TemplateTypeParameterStrategy.cpp src\\TemplateTypeReference.cpp src\\TemplateTypeScope.cpp src\\TemplateTypeVariance.cpp src\\TemplateTypeVarianceMap.cpp src\\TemplateUnionType.cpp src\\ThisType.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinator.cpp src\\TypeCombinatorCache.cpp src\\TypeProjectionHelper.cpp src\\TypeResult.cpp src\\TypeTraits.cpp src\\TypeTraverser.cpp src\\TypeUtils.cpp src\\TypehintHelper.cpp src\\UnionType.cpp src\\UnionTypeHelper.cpp src\\UnresolvedTemplateArgumentType.cpp src\\ValueOfType.cpp src\\VerbosityLevel.cpp src\\VoidType.cpp",
"src\\main.cpp src\\support.cpp src\\AbsorbedTemplateArgumentType.cpp src\\AcceptsResult.cpp src\\AccessoryArrayListType.cpp src\\AccessoryDecimalIntegerStringType.cpp src\\AccessoryLiteralStringType.cpp src\\AccessoryLowercaseStringType.cpp src\\AccessoryNonEmptyStringType.cpp src\\AccessoryNonFalsyStringType.cpp src\\AccessoryNumericStringType.cpp src\\AccessoryUppercaseStringType.cpp src\\ArenaCache.cpp src\\ArrayType.cpp src\\BenevolentUnionType.cpp src\\BooleanType.cpp src\\CallableType.cpp src\\CallableTypeHelper.cpp src\\CircularTypeAliasErrorType.cpp src\\ClassConstantAccessType.cpp src\\ClassReflectionAccess.cpp src\\ClassStringType.cpp src\\ClosureType.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ConditionalType.cpp src\\ConditionalTypeForParameter.cpp src\\ConstantArrayType.cpp src\\ConstantArrayTypeBuilder.cpp src\\ConstantBooleanType.cpp src\\ConstantFloatType.cpp src\\ConstantIntegerType.cpp src\\ConstantStringType.cpp src\\ConstantTypeHelper.cpp src\\EnumCaseObjectType.cpp src\\ErrorType.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\FiniteTypeSet.cpp src\\FloatType.cpp src\\GenericClassStringType.cpp src\\GenericObjectType.cpp src\\GenericStaticType.cpp src\\GetTemplateTypeType.cpp src\\HasMethodType.cpp src\\HasOffsetType.cpp src\\HasOffsetValueType.cpp src\\HasPropertyType.cpp src\\IntegerRangeType.cpp src\\IntegerType.cpp src\\IntersectionType.cpp src\\IsSuperTypeOfResult.cpp src\\IterableType.cpp src\\KeyOfType.cpp src\\LateResolvableArrayShapeType.cpp src\\LruCache.cpp src\\MixedType.cpp src\\NeverType.cpp src\\NewObjectType.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\NonAcceptingNeverType.cpp src\\NonEmptyArrayType.cpp src\\NonexistentParentClassType.cpp src\\NullType.cpp src\\ObjectShapeType.cpp src\\ObjectType.cpp src\\ObjectWithoutClassType.cpp src\\OffsetAccessType.cpp src\\OversizedArrayType.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\RecursionGuard.cpp src\\ResourceType.cpp src\\ScopeContext.cpp src\\ScopeOps.cpp src\\Shadow.cpp src\\StaticType.cpp src\\StaticTypeFactory.cpp src\\StrictMixedType.cpp src\\StringAlwaysAcceptingObjectWithToStringType.cpp src\\StringNeverAcceptingObjectWithToStringType.cpp src\\StringType.cpp src\\SymbolFinderInFiles.cpp src\\TemplateArrayType.cpp src\\TemplateBenevolentUnionType.cpp src\\TemplateBooleanType.cpp src\\TemplateConstantArrayType.cpp src\\TemplateConstantIntegerType.cpp src\\TemplateConstantStringType.cpp src\\TemplateFloatType.cpp src\\TemplateGenericObjectType.cpp src\\TemplateIntegerType.cpp src\\TemplateIntersectionType.cpp src\\TemplateIterableType.cpp src\\TemplateKeyOfType.cpp src\\TemplateMixedType.cpp src\\TemplateNullType.cpp src\\TemplateObjectShapeType.cpp src\\TemplateObjectType.cpp src\\TemplateObjectWithoutClassType.cpp src\\TemplateStrictMixedType.cpp src\\TemplateStringType.cpp src\\TemplateTypeArgumentStrategy.cpp src\\TemplateTypeFactory.cpp src\\TemplateTypeHelper.cpp src\\TemplateTypeMap.cpp src\\TemplateTypeParameterStrategy.cpp src\\TemplateTypeReference.cpp src\\TemplateTypeScope.cpp src\\TemplateTypeVariance.cpp src\\TemplateTypeVarianceMap.cpp src\\TemplateUnionType.cpp src\\ThisType.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinator.cpp src\\TypeCombinatorCache.cpp src\\TypehintHelper.cpp src\\TypeOps.cpp src\\TypeProjectionHelper.cpp src\\TypeResult.cpp src\\TypeTraits.cpp src\\TypeTraverser.cpp src\\TypeUtils.cpp src\\UnionType.cpp src\\UnionTypeHelper.cpp src\\UnresolvedTemplateArgumentType.cpp src\\ValueOfType.cpp src\\VerbosityLevel.cpp src\\VoidType.cpp",
true,
flags);
ADD_SOURCES(configure_module_dirname + "/src/parser",
Expand Down
33 changes: 17 additions & 16 deletions turbo-ext/src/AcceptsResult.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "support.h"
#include "generated/AcceptsResult.h"
#include "zv.h"
#include "TypeTraits.h"

#include <cstring>

Expand Down Expand Up @@ -396,6 +397,15 @@ bool pt_reasons_merge_operands(zval *result, zend_object *self, zval *operands,
* released). */
[[nodiscard]] bool pt_call_fci(zend_fcall_info *fci, zend_fcall_info_cache *fcc, uint32_t argc, zval *argv, zval *retval)
{
/* a callable native code handed out — a holder's __invoke(), a
* TypeTraverser's mapInternal()/traverseInternal() — is entered
* directly (TypeOps.h, pt_direct_invoke) */
const zend_function *fn = fcc->function_handler;
if (fn != NULL && fn->type == ZEND_INTERNAL_FUNCTION) {
bool handled;
bool ok = pt_direct_invoke(fn, fcc->object, argc, argv, retval, handled);
if (handled) return ok;
}
fci->retval = retval;
fci->param_count = argc;
fci->params = argv;
Expand Down Expand Up @@ -621,39 +631,27 @@ using phpstanturbo::AcceptsResult;
* createNo()); owned copy in *out, false = pending exception */
[[nodiscard]] bool pt_accepts_result_singleton(zval *out, zend_long value)
{
zv::Val result = AcceptsResult::singleton(value);
if (UNEXPECTED(result.isUndef())) return false;
*out = result.take();
return true;
return pt_val_into(AcceptsResult::singleton(value), out);
}

/* $self->and($other); false = pending exception */
[[nodiscard]] bool pt_accepts_result_and(zval *out, zval *self, zval *other)
{
zv::Val result = AcceptsResult(Z_OBJ_P(self)).and_(other);
if (UNEXPECTED(result.isUndef())) return false;
*out = result.take();
return true;
return pt_val_into(AcceptsResult(Z_OBJ_P(self)).and_(other), out);
}

/* new AcceptsResult($trinary, $reasons) — IsSuperTypeOfResult::toAcceptsResult();
* reasons is owned and consumed; false = pending exception */
[[nodiscard]] bool pt_accepts_result_create(zval *out, zval *trinary, zval *reasons)
{
zv::Val created = AcceptsResult::create(trinary, zv::Val::adopt(*reasons));
if (UNEXPECTED(created.isUndef())) return false;
*out = created.take();
return true;
return pt_val_into(AcceptsResult::create(trinary, zv::Val::adopt(*reasons)), out);
}

/* $cb($reason) over an array of reasons — the decorateReasons() loop both
* twins share; false = pending exception */
[[nodiscard]] bool pt_reasons_decorate(zval *out, zval *reasons, zend_fcall_info *fci, zend_fcall_info_cache *fcc)
{
zv::Val decorated = AcceptsResult::decorate(zv::ArrRef(reasons), fci, fcc);
if (UNEXPECTED(decorated.isUndef())) return false;
*out = decorated.take();
return true;
return pt_val_into(AcceptsResult::decorate(zv::ArrRef(reasons), fci, fcc), out);
}

void pt_accepts_result_rinit()
Expand Down Expand Up @@ -786,6 +784,9 @@ void pt_register_accepts_result()
cls.method("and", reg::Public, 1, { reg::obj("other", ACCEPTS_RESULT_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) {
pt_accepts_result_and_or(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
});
/* the operand's class checked as Z_PARAM_OBJECT_OF_CLASS does; any other
* object takes the engine path, which raises the TypeError */
cls.op(PT_OP_AND, PT_OP_LAMBDA { if (UNEXPECTED(!instanceof_function(Z_OBJCE_P(argv), pt_ce_accepts_result))) { return pt_type_call_engine(self, "and", sizeof("and") - 1, 1, argv); } return AcceptsResult(self).and_(argv); });

cls.method("or", reg::Public, 1, { reg::obj("other", ACCEPTS_RESULT_CLASS) }, [](INTERNAL_FUNCTION_PARAMETERS) {
pt_accepts_result_and_or(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
Expand Down
Loading