From 8484710513515b454beee8b9912ade948f810d1c Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:20:49 +0000 Subject: [PATCH 1/2] Shadow `TypeTraverser` in the turbo extension, binding its callables once per traversal instead of per node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add turbo-ext/src/TypeTraverser.cpp — PHPStanTurbo\TypeTraverser, the native counterpart of PHPStan\Type\TypeTraverser, with the same map() / mapInternal() / traverseInternal() recursion over the unchanged userland Type::traverse() implementations and callbacks. - Mark PHPStan\Type\TypeTraverser with #[ShadowedByTurboExtension] and PHPStan\Type\TypeTraverserCallable with #[ReferencedByTurboExtension], and register the class-map key in turbo-ext/src/support.{h,cpp}, the registration hook in main.cpp and the source in config.w32. - The port absorbs, per visited node, the mapInternal()/traverseInternal() frames and the [$this, ...] callable array the twin allocates for each of them: both bound callables are built once per traversal and reused, then dropped when map() returns so the traverser's lifetime stays tied to the call. - A TypeTraverserCallable is dispatched straight to traverse(), skipping the adapter closure (and its per-node frame) the twin allocates for it. - One traverser object is parked and reused by the next traversal, but only when nothing outlived the previous one (refcount 1, not weakly referenced) — a callback that kept its $traverse can still call it, and keeps its cb. - Extend turbo-ext/tests/smoke.php with differential coverage over four type shapes and six callback kinds (closure, non-traversing, TypeTraverserCallable, array callable, first-class callable, nested map), plus exception propagation, the non-Type return TypeError and an escaped traverse callable. - Measured on a single-threaded self-analysis (810k map() calls, result cache cleared): inclusive time inside map() drops from 1.270s to 1.09s, ~0.62% of the 28.6s run; six interleaved whole-run A/B pairs all favour the port. - Probed the structural sibling SimultaneousTypeTraverser: it is never called during an analysis run, so it is left in PHP. Closes https://github.com/phpstan/phpstan/issues/15219 --- src/Type/TypeTraverser.php | 3 + src/Type/TypeTraverserCallable.php | 3 + turbo-ext/config.w32 | 2 +- turbo-ext/src/TypeTraverser.cpp | 306 +++++++++++++++++++++++++++++ turbo-ext/src/main.cpp | 2 + turbo-ext/src/support.cpp | 1 + turbo-ext/src/support.h | 4 + turbo-ext/tests/smoke.php | 110 +++++++++++ 8 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 turbo-ext/src/TypeTraverser.cpp diff --git a/src/Type/TypeTraverser.php b/src/Type/TypeTraverser.php index 39674735648..cdeeab97f68 100644 --- a/src/Type/TypeTraverser.php +++ b/src/Type/TypeTraverser.php @@ -2,6 +2,9 @@ namespace PHPStan\Type; +use PHPStan\Turbo\ShadowedByTurboExtension; + +#[ShadowedByTurboExtension(turboClass: 'PHPStanTurbo\TypeTraverser', implementation: __DIR__ . '/../../turbo-ext/src/TypeTraverser.cpp')] final class TypeTraverser { diff --git a/src/Type/TypeTraverserCallable.php b/src/Type/TypeTraverserCallable.php index c8fb374c6d2..bc26fe49dcc 100644 --- a/src/Type/TypeTraverserCallable.php +++ b/src/Type/TypeTraverserCallable.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use PHPStan\Turbo\ReferencedByTurboExtension; + /** * @api */ +#[ReferencedByTurboExtension(key: 'typeTraverserCallable')] interface TypeTraverserCallable { diff --git a/turbo-ext/config.w32 b/turbo-ext/config.w32 index 6d048d369ac..278b11024ff 100644 --- a/turbo-ext/config.w32 +++ b/turbo-ext/config.w32 @@ -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\\ArenaCache.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\ScopeOps.cpp src\\SymbolFinderInFiles.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinatorCache.cpp", + "src\\main.cpp src\\support.cpp src\\ArenaCache.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\ScopeOps.cpp src\\SymbolFinderInFiles.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinatorCache.cpp src\\TypeTraverser.cpp", true, flags); ADD_SOURCES(configure_module_dirname + "/src/parser", diff --git a/turbo-ext/src/TypeTraverser.cpp b/turbo-ext/src/TypeTraverser.cpp new file mode 100644 index 00000000000..df8aea83956 --- /dev/null +++ b/turbo-ext/src/TypeTraverser.cpp @@ -0,0 +1,306 @@ +/* + * PHPStanTurbo\TypeTraverser — native implementation of + * PHPStan\Type\TypeTraverser. + * + * Not final: the generated stub PHPStan\Type\TypeTraverser extends this + * class, and map() instantiates the called scope — the stub — so the + * traverser userland callbacks receive is the class PHPStan's own code + * knows. State lives in the PHP object's cb property, like the twin's. + * + * The recursion is the twin's, method for method: map() creates a traverser, + * mapInternal() calls the user callback with a bound traverseInternal(), and + * traverseInternal() hands mapInternal() to Type::traverse(). What the port + * absorbs is the userland overhead the twin pays around those two calls — + * per visited node a mapInternal() and a traverseInternal() frame plus a + * freshly allocated [$this, ...] callable array each, and, for a + * TypeTraverserCallable, one closure frame on top. Here both bound callables + * are built once per traversal and reused for every node in it, and a + * TypeTraverserCallable is dispatched straight to traverse(). + */ + +#include "support.h" +#include "zv.h" + +/* declaration order in pt_register_type_traverser() */ +#define PT_TT_PROP_CB 0 +#define PT_TT_PROP_MAP_CALLABLE 1 +#define PT_TT_PROP_TRAVERSE_CALLABLE 2 + +/* the traverser kept for the next traversal (see park()); owns a reference */ +static zend_object *pt_tt_parked = nullptr; + +/* method names of the bound callables; permanent interned strings */ +static zend_string *pt_str_map_internal = nullptr; +static zend_string *pt_str_traverse_internal = nullptr; + +namespace phpstanturbo { + +/* Mirrors PHPStan\Type\TypeTraverser. */ +class TypeTraverser +{ +public: + explicit TypeTraverser(zval *self) : self(self) {} + + /* UNDEF result means a pending exception */ + static zv::Val map(zend_class_entry *scope, zval *type, zval *cb) + { + zval selfZv; + zend_object *reused = takeParked(scope); + if (reused != NULL) { + ZVAL_OBJ(&selfZv, reused); + } else if (UNEXPECTED(object_init_ex(&selfZv, scope) != SUCCESS)) { + return zv::Val(); + } + + zv::Val owned = zv::Val::adopt(selfZv); + TypeTraverser traverser(owned.raw()); + traverser.construct(zv::Ref(cb)); + zv::Val result = traverser.mapInternal(type); + /* the bound callables hold the traverser; dropping them here is what + * keeps the object's lifetime tied to this call, as the twin's is */ + traverser.releaseBoundCallables(); + traverser.parkIfUnescaped(); + + return result; + } + + void construct(zv::Ref cb) + { + zv::ObjRef(self).propAtWrite(PT_TT_PROP_CB, zv::Val::copyOf(cb)); + } + + /* ($this->cb)($type, [$this, 'traverseInternal']); UNDEF = pending exception */ + zv::Val mapInternal(zval *type) + { + zval *cb = OBJ_PROP_NUM(Z_OBJ_P(self), PT_TT_PROP_CB); + bool isTraverserCallable = false; + /* the twin wraps a TypeTraverserCallable in a closure once per + * traversal; calling traverse() directly saves that frame per node. + * Closures are the common cb and never implement the interface. */ + if (Z_TYPE_P(cb) == IS_OBJECT && Z_OBJCE_P(cb) != zend_ce_closure) { + zend_class_entry *iface = pt_class(PT_CLASS_TYPE_TRAVERSER_CALLABLE); + if (UNEXPECTED(iface == NULL)) { + return zv::Val(); + } + isTraverserCallable = instanceof_function(Z_OBJCE_P(cb), iface); + } + + zval *traverse = boundCallable(PT_TT_PROP_TRAVERSE_CALLABLE, pt_str_traverse_internal); + zval args[2]; + ZVAL_COPY_VALUE(&args[0], type); + ZVAL_COPY_VALUE(&args[1], traverse); + + zval retval; + ZVAL_UNDEF(&retval); + if (isTraverserCallable) { + zend_function *fn = pt_find_method(Z_OBJCE_P(cb), "traverse", sizeof("traverse") - 1); + if (UNEXPECTED(fn == NULL)) { + return zv::Val(); + } + zend_call_known_instance_method(fn, Z_OBJ_P(cb), &retval, 2, args); + } else if (UNEXPECTED(call_user_function(NULL, NULL, cb, &retval, 2, args) != SUCCESS)) { + zval_ptr_dtor(&retval); + return zv::Val(); + } + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&retval); + return zv::Val(); + } + /* the twin's ": Type" return type; only the object-ness is checked + * here — what the value is gets decided by the Type call it feeds, + * and an instanceof against the interface would cost a per-node + * interface-table scan */ + if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT)) { + zval_ptr_dtor(&retval); + zend_class_entry *typeCe = pt_class(PT_CLASS_TYPE); + if (EXPECTED(typeCe != NULL)) { + zend_type_error("Return value of the callback must be of type %s", ZSTR_VAL(typeCe->name)); + } + return zv::Val(); + } + + return zv::Val::adopt(retval); + } + + /* $type->traverse([$this, 'mapInternal']); UNDEF = pending exception */ + zv::Val traverseInternal(zval *type) + { + zend_function *fn = pt_find_method(Z_OBJCE_P(type), "traverse", sizeof("traverse") - 1); + if (UNEXPECTED(fn == NULL)) { + return zv::Val(); + } + + zval arg; + ZVAL_COPY_VALUE(&arg, boundCallable(PT_TT_PROP_MAP_CALLABLE, pt_str_map_internal)); + + zval retval; + ZVAL_UNDEF(&retval); + zend_call_known_instance_method(fn, Z_OBJ_P(type), &retval, 1, &arg); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&retval); + return zv::Val(); + } + + return zv::Val::adopt(retval); + } + +private: + zval *self; + + /* + * [$this, $method], built on first use and reused for the rest of the + * traversal — where the twin allocates one array per node visit. The + * returned zval is borrowed from the property slot; callers pass it on as + * an argument, which copies it. + */ + zval *boundCallable(uint32_t slot, zend_string *method) + { + zend_object *obj = Z_OBJ_P(self); + zval *cached = OBJ_PROP_NUM(obj, slot); + if (EXPECTED(Z_TYPE_P(cached) == IS_ARRAY)) { + return cached; + } + + zval pair, entry; + array_init_size(&pair, 2); + ZVAL_OBJ_COPY(&entry, obj); + zend_hash_next_index_insert_new(Z_ARRVAL(pair), &entry); + ZVAL_STR_COPY(&entry, method); + zend_hash_next_index_insert_new(Z_ARRVAL(pair), &entry); + zv::ObjRef(obj).propAtWrite(slot, zv::Val::adopt(pair)); + + return OBJ_PROP_NUM(obj, slot); + } + + void releaseBoundCallables() + { + zv::ObjRef obj(self); + obj.propAtWrite(PT_TT_PROP_MAP_CALLABLE, zv::Val::null()); + obj.propAtWrite(PT_TT_PROP_TRAVERSE_CALLABLE, zv::Val::null()); + } + + /* + * Keeps the traverser for the next traversal instead of letting map() + * free it: an analysis runs millions of traversals, each of which would + * otherwise allocate and free one object. + * + * Only when nothing outlives the traversal. Refcount 1 is map()'s own + * reference — with the bound callables already dropped, anything else + * means the callback kept one of them (a $traverse it can still call, + * which needs its cb, so that one is left to die with the traverser). + * A WeakReference holds no reference but would observe the reuse. + */ + void parkIfUnescaped() + { + zend_object *obj = Z_OBJ_P(self); + if (pt_tt_parked != NULL || GC_REFCOUNT(obj) != 1 || (GC_FLAGS(obj) & IS_OBJ_WEAKLY_REFERENCED)) { + return; + } + zv::ObjRef(obj).propAtWrite(PT_TT_PROP_CB, zv::Val::null()); + pt_tt_parked = obj; + GC_ADDREF(obj); + } + + /* the parked traverser, if it fits the requested class; its slot stays + * empty while it is in use, so nested traversals allocate their own */ + static zend_object *takeParked(zend_class_entry *scope) + { + zend_object *parked = pt_tt_parked; + if (parked == NULL || parked->ce != scope) { + return NULL; + } + pt_tt_parked = NULL; + + return parked; + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::TypeTraverser; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +#include "reg.h" + +void pt_type_traverser_rshutdown() +{ + if (pt_tt_parked != NULL) { + zend_object_release(pt_tt_parked); + pt_tt_parked = NULL; + } +} + +void pt_register_type_traverser() +{ + pt_str_map_internal = zend_string_init_interned("mapInternal", sizeof("mapInternal") - 1, 1); + pt_str_traverse_internal = zend_string_init_interned("traverseInternal", sizeof("traverseInternal") - 1, 1); + + reg::Class cls("PHPStanTurbo\\TypeTraverser"); + /* not final: the stub subclass PHPStan\Type\TypeTraverser extends this + * class; "cb" must stay the first declared property (OBJ_PROP_NUM slot 0). + * The two callable slots have no counterpart in the twin — they are the + * traversal's memo of the bound callables it allocates per node. */ + cls.privateNullProperty("cb"); + cls.privateNullProperty("mapCallable"); + cls.privateNullProperty("traverseCallable"); + + cls.method("map", reg::PublicStatic, 2, { reg::any("type"), reg::any("cb") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type, *cb; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT(type) + Z_PARAM_ZVAL(cb) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_DEREF(cb); + /* the called scope is the stub subclass, so the traverser is the class + * PHPStan's own code knows; the declaring class covers the callable + * forms the engine invokes without one */ + zend_class_entry *scope = zend_get_called_scope(execute_data); + zv::Val result = TypeTraverser::map(scope != NULL ? scope : execute_data->func->common.scope, type, cb); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("__construct", reg::Private, 1, { reg::any("cb") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *cb; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(cb) + ZEND_PARSE_PARAMETERS_END(); + + ZVAL_DEREF(cb); + TypeTraverser(ZEND_THIS).construct(zv::Ref(cb)); + }); + + cls.method("mapInternal", reg::Public, 1, { reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(type) + ZEND_PARSE_PARAMETERS_END(); + + zv::Val result = TypeTraverser(ZEND_THIS).mapInternal(type); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.method("traverseInternal", reg::Public, 1, { reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT(type) + ZEND_PARSE_PARAMETERS_END(); + + zv::Val result = TypeTraverser(ZEND_THIS).traverseInternal(type); + if (UNEXPECTED(result.isUndef())) { + RETURN_THROWS(); + } + result.intoReturnValue(return_value); + }); + + cls.register_(); +} + +/* }}} */ diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index bd8f97fdc4f..229bcf5547f 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -146,6 +146,7 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) pt_register_node_scanner(); pt_register_parser_runner(); pt_register_type_combinator_cache(); + pt_register_type_traverser(); pt_register_arena_cache(); pt_register_expression_result_storage(); pt_register_php_file_cleaner(); @@ -180,6 +181,7 @@ static PHP_RSHUTDOWN_FUNCTION(phpstan_turbo) pt_scope_ops_rshutdown(); pt_node_traverser_rshutdown(); pt_type_combinator_cache_rshutdown(); + pt_type_traverser_rshutdown(); pt_support_rshutdown(); return SUCCESS; diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 0fad5118070..481348b8fd5 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -49,6 +49,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_ARROW_FUNCTION */ {"arrowFunction", "PhpParser\\Node\\Expr\\ArrowFunction"}, /* PT_CLASS_TYPE */ {"type", "PHPStan\\Type\\Type"}, /* PT_CLASS_RECURSION_GUARD */ {"recursionGuard", "PHPStan\\Type\\RecursionGuard"}, + /* PT_CLASS_TYPE_TRAVERSER_CALLABLE */ {"typeTraverserCallable", "PHPStan\\Type\\TypeTraverserCallable"}, /* PT_CLASS_TRINARY */ {"trinaryLogic", NULL}, /* PT_CLASS_ETH */ {"expressionTypeHolder", NULL}, /* PT_CLASS_CEH */ {"conditionalExpressionHolder", NULL}, diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 4d04b506a0c..3876573f10b 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -31,6 +31,7 @@ extern "C" { #include "php.h" +#include "zend_closures.h" #include "zend_exceptions.h" #include "zend_interfaces.h" #include "zend_smart_str.h" @@ -81,6 +82,7 @@ enum { PT_CLASS_ARROW_FUNCTION, PT_CLASS_TYPE, PT_CLASS_RECURSION_GUARD, + PT_CLASS_TYPE_TRAVERSER_CALLABLE, /* classes the extension instantiates (their PHP twins are themselves * shadowed, hence no default name): configured to the stub subclasses * so created objects satisfy the original PHPStan type hints */ @@ -150,6 +152,7 @@ void pt_register_scope_ops(); void pt_register_node_scanner(); void pt_register_parser_runner(); void pt_register_type_combinator_cache(); +void pt_register_type_traverser(); void pt_register_arena_cache(); void pt_register_expression_result_storage(); void pt_register_php_file_cleaner(); @@ -162,6 +165,7 @@ void pt_scope_ops_rinit(); void pt_scope_ops_rshutdown(); void pt_type_combinator_cache_rinit(); void pt_type_combinator_cache_rshutdown(); +void pt_type_traverser_rshutdown(); /* module-shutdown backstop: destroys the arena mapping if the run skipped * ArenaCache::destroy() on a graceful exit */ diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index 583493caf26..8619286c313 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -304,6 +304,116 @@ function check(bool $cond, string $msg): void check($describe($afterClear) === $describe($first), 'TCC clearCache keeps results correct'); check($afterClear !== $first, 'TCC clearCache actually drops entries'); +// ---- TypeTraverser ---- +$covered[\PHPStan\Type\TypeTraverser::class] = true; +// The native traverser drives the same recursion through the same userland +// Type::traverse() implementations and the same callbacks; only the frames +// in between are native, so identical results are the whole contract. +$ttPrecise = \PHPStan\Type\VerbosityLevel::precise(); +$ttInputs = [ + 'leaf' => $stringT, + 'array' => new \PHPStan\Type\ArrayType($stringT, $intT), + 'union' => \PHPStan\Type\TypeCombinator::union($oneT, $nullT, new \PHPStan\Type\Constant\ConstantStringType('a')), + 'nested' => new \PHPStan\Type\ArrayType(new \PHPStan\Type\MixedType(), \PHPStan\Type\TypeCombinator::intersect($arrayT, $nonEmpty)), +]; + +final class SmokeTypeTraverserCallable implements \PHPStan\Type\TypeTraverserCallable +{ + + public function traverse(\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type + { + if ($type->isInteger()->yes()) { + return new \PHPStan\Type\BooleanType(); + } + return $traverse($type); + } + +} + +final class SmokeTypeTraverserMethod +{ + + public function widenStrings(\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type + { + if ($type->isString()->yes()) { + return new \PHPStan\Type\NullType(); + } + return $traverse($type); + } + +} + +$ttMethodCb = new SmokeTypeTraverserMethod(); +$ttCallbacks = [ + // the documented example: constant strings to objects, unions traversed + 'closure' => static function (\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type { + if ($type instanceof \PHPStan\Type\UnionType || $type instanceof \PHPStan\Type\IntersectionType) { + return $traverse($type); + } + if ($type instanceof \PHPStan\Type\Constant\ConstantStringType) { + return new \PHPStan\Type\ObjectType($type->getValue()); + } + return new \PHPStan\Type\MixedType(); + }, + // never traverses: the callback decides the whole result + 'no traverse' => static fn (\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type => new \PHPStan\Type\NullType(), + // the twin wraps this one in a closure, the native side calls traverse() directly + 'TypeTraverserCallable' => new SmokeTypeTraverserCallable(), + 'array callable' => [$ttMethodCb, 'widenStrings'], + 'first-class callable' => $ttMethodCb->widenStrings(...), + // a traversal started from inside a traversal, which the traverser the + // native side keeps for reuse must not disturb + 'nested map' => static function (\PHPStan\Type\Type $type, callable $traverse) use ($intT): \PHPStan\Type\Type { + if ($type->isString()->yes()) { + return \PHPStan\Type\TypeTraverser::map(new \PHPStan\Type\ArrayType($type, $intT), static fn (\PHPStan\Type\Type $inner, callable $innerTraverse): \PHPStan\Type\Type => $inner->isInteger()->yes() ? new \PHPStan\Type\FloatType() : $innerTraverse($inner)); + } + return $traverse($type); + }, +]; + +foreach ($ttCallbacks as $ttLabel => $ttCallback) { + foreach ($ttInputs as $ttInputLabel => $ttInput) { + $phpResult = \PHPStan\Type\TypeTraverser::map($ttInput, $ttCallback)->describe($ttPrecise); + $nativeResult = \PHPStanTurbo\TypeTraverser::map($ttInput, $ttCallback)->describe($ttPrecise); + check($phpResult === $nativeResult, "TypeTraverser $ttLabel over $ttInputLabel: $phpResult vs $nativeResult"); + } +} + +// an exception from the callback propagates out of map() +foreach (['php' => \PHPStan\Type\TypeTraverser::class, 'native' => \PHPStanTurbo\TypeTraverser::class] as $ttSide => $ttClass) { + $ttThrown = null; + try { + $ttClass::map($ttInputs['array'], static function (\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type { + throw new \PHPStan\ShouldNotHappenException(); + }); + } catch (\PHPStan\ShouldNotHappenException $e) { + $ttThrown = $e; + } + check($ttThrown !== null, "TypeTraverser $ttSide: the callback's exception propagates"); + + // a callback that returns something else than a Type is a TypeError on + // both sides (the twin's declared return type, checked natively) + $ttTypeError = null; + try { + $ttClass::map($ttInputs['leaf'], static fn (\PHPStan\Type\Type $type, callable $traverse) => 'not a type'); + } catch (TypeError $e) { + $ttTypeError = $e; + } + check($ttTypeError !== null, "TypeTraverser $ttSide: a non-Type callback result is a TypeError"); + + // the traverse callable stays usable after map() returned — the native + // traverser must not recycle an object the callback kept a hold of + $ttEscaped = null; + $ttClass::map($ttInputs['array'], static function (\PHPStan\Type\Type $type, callable $traverse) use (&$ttEscaped): \PHPStan\Type\Type { + $ttEscaped ??= $traverse; + return $traverse($type); + }); + check( + $ttEscaped(new \PHPStan\Type\ArrayType($stringT, $intT))->describe($ttPrecise) === 'array', + "TypeTraverser $ttSide: an escaped traverse callable still works", + ); +} + // ---- ExpressionResultStorage ---- $covered[\PHPStan\Analyser\ExpressionResultStorage::class] = true; $makeScope = static function () { From 1c80c28add228e8b7898a52bbfcaf0728ddbb3f3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 12 Sep 2026 13:25:38 +0000 Subject: [PATCH 2/2] Revert "Shadow `TypeTraverser` in the turbo extension, binding its callables once per traversal instead of per node" This reverts commit 8484710513515b454beee8b9912ade948f810d1c. --- src/Type/TypeTraverser.php | 3 - src/Type/TypeTraverserCallable.php | 3 - turbo-ext/config.w32 | 2 +- turbo-ext/src/TypeTraverser.cpp | 306 ----------------------------- turbo-ext/src/main.cpp | 2 - turbo-ext/src/support.cpp | 1 - turbo-ext/src/support.h | 4 - turbo-ext/tests/smoke.php | 110 ----------- 8 files changed, 1 insertion(+), 430 deletions(-) delete mode 100644 turbo-ext/src/TypeTraverser.cpp diff --git a/src/Type/TypeTraverser.php b/src/Type/TypeTraverser.php index cdeeab97f68..39674735648 100644 --- a/src/Type/TypeTraverser.php +++ b/src/Type/TypeTraverser.php @@ -2,9 +2,6 @@ namespace PHPStan\Type; -use PHPStan\Turbo\ShadowedByTurboExtension; - -#[ShadowedByTurboExtension(turboClass: 'PHPStanTurbo\TypeTraverser', implementation: __DIR__ . '/../../turbo-ext/src/TypeTraverser.cpp')] final class TypeTraverser { diff --git a/src/Type/TypeTraverserCallable.php b/src/Type/TypeTraverserCallable.php index bc26fe49dcc..c8fb374c6d2 100644 --- a/src/Type/TypeTraverserCallable.php +++ b/src/Type/TypeTraverserCallable.php @@ -2,12 +2,9 @@ namespace PHPStan\Type; -use PHPStan\Turbo\ReferencedByTurboExtension; - /** * @api */ -#[ReferencedByTurboExtension(key: 'typeTraverserCallable')] interface TypeTraverserCallable { diff --git a/turbo-ext/config.w32 b/turbo-ext/config.w32 index 278b11024ff..6d048d369ac 100644 --- a/turbo-ext/config.w32 +++ b/turbo-ext/config.w32 @@ -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\\ArenaCache.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\ScopeOps.cpp src\\SymbolFinderInFiles.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinatorCache.cpp src\\TypeTraverser.cpp", + "src\\main.cpp src\\support.cpp src\\ArenaCache.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\ScopeOps.cpp src\\SymbolFinderInFiles.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinatorCache.cpp", true, flags); ADD_SOURCES(configure_module_dirname + "/src/parser", diff --git a/turbo-ext/src/TypeTraverser.cpp b/turbo-ext/src/TypeTraverser.cpp deleted file mode 100644 index df8aea83956..00000000000 --- a/turbo-ext/src/TypeTraverser.cpp +++ /dev/null @@ -1,306 +0,0 @@ -/* - * PHPStanTurbo\TypeTraverser — native implementation of - * PHPStan\Type\TypeTraverser. - * - * Not final: the generated stub PHPStan\Type\TypeTraverser extends this - * class, and map() instantiates the called scope — the stub — so the - * traverser userland callbacks receive is the class PHPStan's own code - * knows. State lives in the PHP object's cb property, like the twin's. - * - * The recursion is the twin's, method for method: map() creates a traverser, - * mapInternal() calls the user callback with a bound traverseInternal(), and - * traverseInternal() hands mapInternal() to Type::traverse(). What the port - * absorbs is the userland overhead the twin pays around those two calls — - * per visited node a mapInternal() and a traverseInternal() frame plus a - * freshly allocated [$this, ...] callable array each, and, for a - * TypeTraverserCallable, one closure frame on top. Here both bound callables - * are built once per traversal and reused for every node in it, and a - * TypeTraverserCallable is dispatched straight to traverse(). - */ - -#include "support.h" -#include "zv.h" - -/* declaration order in pt_register_type_traverser() */ -#define PT_TT_PROP_CB 0 -#define PT_TT_PROP_MAP_CALLABLE 1 -#define PT_TT_PROP_TRAVERSE_CALLABLE 2 - -/* the traverser kept for the next traversal (see park()); owns a reference */ -static zend_object *pt_tt_parked = nullptr; - -/* method names of the bound callables; permanent interned strings */ -static zend_string *pt_str_map_internal = nullptr; -static zend_string *pt_str_traverse_internal = nullptr; - -namespace phpstanturbo { - -/* Mirrors PHPStan\Type\TypeTraverser. */ -class TypeTraverser -{ -public: - explicit TypeTraverser(zval *self) : self(self) {} - - /* UNDEF result means a pending exception */ - static zv::Val map(zend_class_entry *scope, zval *type, zval *cb) - { - zval selfZv; - zend_object *reused = takeParked(scope); - if (reused != NULL) { - ZVAL_OBJ(&selfZv, reused); - } else if (UNEXPECTED(object_init_ex(&selfZv, scope) != SUCCESS)) { - return zv::Val(); - } - - zv::Val owned = zv::Val::adopt(selfZv); - TypeTraverser traverser(owned.raw()); - traverser.construct(zv::Ref(cb)); - zv::Val result = traverser.mapInternal(type); - /* the bound callables hold the traverser; dropping them here is what - * keeps the object's lifetime tied to this call, as the twin's is */ - traverser.releaseBoundCallables(); - traverser.parkIfUnescaped(); - - return result; - } - - void construct(zv::Ref cb) - { - zv::ObjRef(self).propAtWrite(PT_TT_PROP_CB, zv::Val::copyOf(cb)); - } - - /* ($this->cb)($type, [$this, 'traverseInternal']); UNDEF = pending exception */ - zv::Val mapInternal(zval *type) - { - zval *cb = OBJ_PROP_NUM(Z_OBJ_P(self), PT_TT_PROP_CB); - bool isTraverserCallable = false; - /* the twin wraps a TypeTraverserCallable in a closure once per - * traversal; calling traverse() directly saves that frame per node. - * Closures are the common cb and never implement the interface. */ - if (Z_TYPE_P(cb) == IS_OBJECT && Z_OBJCE_P(cb) != zend_ce_closure) { - zend_class_entry *iface = pt_class(PT_CLASS_TYPE_TRAVERSER_CALLABLE); - if (UNEXPECTED(iface == NULL)) { - return zv::Val(); - } - isTraverserCallable = instanceof_function(Z_OBJCE_P(cb), iface); - } - - zval *traverse = boundCallable(PT_TT_PROP_TRAVERSE_CALLABLE, pt_str_traverse_internal); - zval args[2]; - ZVAL_COPY_VALUE(&args[0], type); - ZVAL_COPY_VALUE(&args[1], traverse); - - zval retval; - ZVAL_UNDEF(&retval); - if (isTraverserCallable) { - zend_function *fn = pt_find_method(Z_OBJCE_P(cb), "traverse", sizeof("traverse") - 1); - if (UNEXPECTED(fn == NULL)) { - return zv::Val(); - } - zend_call_known_instance_method(fn, Z_OBJ_P(cb), &retval, 2, args); - } else if (UNEXPECTED(call_user_function(NULL, NULL, cb, &retval, 2, args) != SUCCESS)) { - zval_ptr_dtor(&retval); - return zv::Val(); - } - if (UNEXPECTED(EG(exception))) { - zval_ptr_dtor(&retval); - return zv::Val(); - } - /* the twin's ": Type" return type; only the object-ness is checked - * here — what the value is gets decided by the Type call it feeds, - * and an instanceof against the interface would cost a per-node - * interface-table scan */ - if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT)) { - zval_ptr_dtor(&retval); - zend_class_entry *typeCe = pt_class(PT_CLASS_TYPE); - if (EXPECTED(typeCe != NULL)) { - zend_type_error("Return value of the callback must be of type %s", ZSTR_VAL(typeCe->name)); - } - return zv::Val(); - } - - return zv::Val::adopt(retval); - } - - /* $type->traverse([$this, 'mapInternal']); UNDEF = pending exception */ - zv::Val traverseInternal(zval *type) - { - zend_function *fn = pt_find_method(Z_OBJCE_P(type), "traverse", sizeof("traverse") - 1); - if (UNEXPECTED(fn == NULL)) { - return zv::Val(); - } - - zval arg; - ZVAL_COPY_VALUE(&arg, boundCallable(PT_TT_PROP_MAP_CALLABLE, pt_str_map_internal)); - - zval retval; - ZVAL_UNDEF(&retval); - zend_call_known_instance_method(fn, Z_OBJ_P(type), &retval, 1, &arg); - if (UNEXPECTED(EG(exception))) { - zval_ptr_dtor(&retval); - return zv::Val(); - } - - return zv::Val::adopt(retval); - } - -private: - zval *self; - - /* - * [$this, $method], built on first use and reused for the rest of the - * traversal — where the twin allocates one array per node visit. The - * returned zval is borrowed from the property slot; callers pass it on as - * an argument, which copies it. - */ - zval *boundCallable(uint32_t slot, zend_string *method) - { - zend_object *obj = Z_OBJ_P(self); - zval *cached = OBJ_PROP_NUM(obj, slot); - if (EXPECTED(Z_TYPE_P(cached) == IS_ARRAY)) { - return cached; - } - - zval pair, entry; - array_init_size(&pair, 2); - ZVAL_OBJ_COPY(&entry, obj); - zend_hash_next_index_insert_new(Z_ARRVAL(pair), &entry); - ZVAL_STR_COPY(&entry, method); - zend_hash_next_index_insert_new(Z_ARRVAL(pair), &entry); - zv::ObjRef(obj).propAtWrite(slot, zv::Val::adopt(pair)); - - return OBJ_PROP_NUM(obj, slot); - } - - void releaseBoundCallables() - { - zv::ObjRef obj(self); - obj.propAtWrite(PT_TT_PROP_MAP_CALLABLE, zv::Val::null()); - obj.propAtWrite(PT_TT_PROP_TRAVERSE_CALLABLE, zv::Val::null()); - } - - /* - * Keeps the traverser for the next traversal instead of letting map() - * free it: an analysis runs millions of traversals, each of which would - * otherwise allocate and free one object. - * - * Only when nothing outlives the traversal. Refcount 1 is map()'s own - * reference — with the bound callables already dropped, anything else - * means the callback kept one of them (a $traverse it can still call, - * which needs its cb, so that one is left to die with the traverser). - * A WeakReference holds no reference but would observe the reuse. - */ - void parkIfUnescaped() - { - zend_object *obj = Z_OBJ_P(self); - if (pt_tt_parked != NULL || GC_REFCOUNT(obj) != 1 || (GC_FLAGS(obj) & IS_OBJ_WEAKLY_REFERENCED)) { - return; - } - zv::ObjRef(obj).propAtWrite(PT_TT_PROP_CB, zv::Val::null()); - pt_tt_parked = obj; - GC_ADDREF(obj); - } - - /* the parked traverser, if it fits the requested class; its slot stays - * empty while it is in use, so nested traversals allocate their own */ - static zend_object *takeParked(zend_class_entry *scope) - { - zend_object *parked = pt_tt_parked; - if (parked == NULL || parked->ce != scope) { - return NULL; - } - pt_tt_parked = NULL; - - return parked; - } -}; - -} // namespace phpstanturbo - -using phpstanturbo::TypeTraverser; - -/* {{{ engine ABI glue: parameter parsing + registration */ - -#include "reg.h" - -void pt_type_traverser_rshutdown() -{ - if (pt_tt_parked != NULL) { - zend_object_release(pt_tt_parked); - pt_tt_parked = NULL; - } -} - -void pt_register_type_traverser() -{ - pt_str_map_internal = zend_string_init_interned("mapInternal", sizeof("mapInternal") - 1, 1); - pt_str_traverse_internal = zend_string_init_interned("traverseInternal", sizeof("traverseInternal") - 1, 1); - - reg::Class cls("PHPStanTurbo\\TypeTraverser"); - /* not final: the stub subclass PHPStan\Type\TypeTraverser extends this - * class; "cb" must stay the first declared property (OBJ_PROP_NUM slot 0). - * The two callable slots have no counterpart in the twin — they are the - * traversal's memo of the bound callables it allocates per node. */ - cls.privateNullProperty("cb"); - cls.privateNullProperty("mapCallable"); - cls.privateNullProperty("traverseCallable"); - - cls.method("map", reg::PublicStatic, 2, { reg::any("type"), reg::any("cb") }, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *type, *cb; - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_OBJECT(type) - Z_PARAM_ZVAL(cb) - ZEND_PARSE_PARAMETERS_END(); - - ZVAL_DEREF(cb); - /* the called scope is the stub subclass, so the traverser is the class - * PHPStan's own code knows; the declaring class covers the callable - * forms the engine invokes without one */ - zend_class_entry *scope = zend_get_called_scope(execute_data); - zv::Val result = TypeTraverser::map(scope != NULL ? scope : execute_data->func->common.scope, type, cb); - if (UNEXPECTED(result.isUndef())) { - RETURN_THROWS(); - } - result.intoReturnValue(return_value); - }); - - cls.method("__construct", reg::Private, 1, { reg::any("cb") }, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *cb; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_ZVAL(cb) - ZEND_PARSE_PARAMETERS_END(); - - ZVAL_DEREF(cb); - TypeTraverser(ZEND_THIS).construct(zv::Ref(cb)); - }); - - cls.method("mapInternal", reg::Public, 1, { reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *type; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_OBJECT(type) - ZEND_PARSE_PARAMETERS_END(); - - zv::Val result = TypeTraverser(ZEND_THIS).mapInternal(type); - if (UNEXPECTED(result.isUndef())) { - RETURN_THROWS(); - } - result.intoReturnValue(return_value); - }); - - cls.method("traverseInternal", reg::Public, 1, { reg::any("type") }, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *type; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_OBJECT(type) - ZEND_PARSE_PARAMETERS_END(); - - zv::Val result = TypeTraverser(ZEND_THIS).traverseInternal(type); - if (UNEXPECTED(result.isUndef())) { - RETURN_THROWS(); - } - result.intoReturnValue(return_value); - }); - - cls.register_(); -} - -/* }}} */ diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index 229bcf5547f..bd8f97fdc4f 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -146,7 +146,6 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) pt_register_node_scanner(); pt_register_parser_runner(); pt_register_type_combinator_cache(); - pt_register_type_traverser(); pt_register_arena_cache(); pt_register_expression_result_storage(); pt_register_php_file_cleaner(); @@ -181,7 +180,6 @@ static PHP_RSHUTDOWN_FUNCTION(phpstan_turbo) pt_scope_ops_rshutdown(); pt_node_traverser_rshutdown(); pt_type_combinator_cache_rshutdown(); - pt_type_traverser_rshutdown(); pt_support_rshutdown(); return SUCCESS; diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 481348b8fd5..0fad5118070 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -49,7 +49,6 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_ARROW_FUNCTION */ {"arrowFunction", "PhpParser\\Node\\Expr\\ArrowFunction"}, /* PT_CLASS_TYPE */ {"type", "PHPStan\\Type\\Type"}, /* PT_CLASS_RECURSION_GUARD */ {"recursionGuard", "PHPStan\\Type\\RecursionGuard"}, - /* PT_CLASS_TYPE_TRAVERSER_CALLABLE */ {"typeTraverserCallable", "PHPStan\\Type\\TypeTraverserCallable"}, /* PT_CLASS_TRINARY */ {"trinaryLogic", NULL}, /* PT_CLASS_ETH */ {"expressionTypeHolder", NULL}, /* PT_CLASS_CEH */ {"conditionalExpressionHolder", NULL}, diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 3876573f10b..4d04b506a0c 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -31,7 +31,6 @@ extern "C" { #include "php.h" -#include "zend_closures.h" #include "zend_exceptions.h" #include "zend_interfaces.h" #include "zend_smart_str.h" @@ -82,7 +81,6 @@ enum { PT_CLASS_ARROW_FUNCTION, PT_CLASS_TYPE, PT_CLASS_RECURSION_GUARD, - PT_CLASS_TYPE_TRAVERSER_CALLABLE, /* classes the extension instantiates (their PHP twins are themselves * shadowed, hence no default name): configured to the stub subclasses * so created objects satisfy the original PHPStan type hints */ @@ -152,7 +150,6 @@ void pt_register_scope_ops(); void pt_register_node_scanner(); void pt_register_parser_runner(); void pt_register_type_combinator_cache(); -void pt_register_type_traverser(); void pt_register_arena_cache(); void pt_register_expression_result_storage(); void pt_register_php_file_cleaner(); @@ -165,7 +162,6 @@ void pt_scope_ops_rinit(); void pt_scope_ops_rshutdown(); void pt_type_combinator_cache_rinit(); void pt_type_combinator_cache_rshutdown(); -void pt_type_traverser_rshutdown(); /* module-shutdown backstop: destroys the arena mapping if the run skipped * ArenaCache::destroy() on a graceful exit */ diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index 8619286c313..583493caf26 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -304,116 +304,6 @@ function check(bool $cond, string $msg): void check($describe($afterClear) === $describe($first), 'TCC clearCache keeps results correct'); check($afterClear !== $first, 'TCC clearCache actually drops entries'); -// ---- TypeTraverser ---- -$covered[\PHPStan\Type\TypeTraverser::class] = true; -// The native traverser drives the same recursion through the same userland -// Type::traverse() implementations and the same callbacks; only the frames -// in between are native, so identical results are the whole contract. -$ttPrecise = \PHPStan\Type\VerbosityLevel::precise(); -$ttInputs = [ - 'leaf' => $stringT, - 'array' => new \PHPStan\Type\ArrayType($stringT, $intT), - 'union' => \PHPStan\Type\TypeCombinator::union($oneT, $nullT, new \PHPStan\Type\Constant\ConstantStringType('a')), - 'nested' => new \PHPStan\Type\ArrayType(new \PHPStan\Type\MixedType(), \PHPStan\Type\TypeCombinator::intersect($arrayT, $nonEmpty)), -]; - -final class SmokeTypeTraverserCallable implements \PHPStan\Type\TypeTraverserCallable -{ - - public function traverse(\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type - { - if ($type->isInteger()->yes()) { - return new \PHPStan\Type\BooleanType(); - } - return $traverse($type); - } - -} - -final class SmokeTypeTraverserMethod -{ - - public function widenStrings(\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type - { - if ($type->isString()->yes()) { - return new \PHPStan\Type\NullType(); - } - return $traverse($type); - } - -} - -$ttMethodCb = new SmokeTypeTraverserMethod(); -$ttCallbacks = [ - // the documented example: constant strings to objects, unions traversed - 'closure' => static function (\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type { - if ($type instanceof \PHPStan\Type\UnionType || $type instanceof \PHPStan\Type\IntersectionType) { - return $traverse($type); - } - if ($type instanceof \PHPStan\Type\Constant\ConstantStringType) { - return new \PHPStan\Type\ObjectType($type->getValue()); - } - return new \PHPStan\Type\MixedType(); - }, - // never traverses: the callback decides the whole result - 'no traverse' => static fn (\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type => new \PHPStan\Type\NullType(), - // the twin wraps this one in a closure, the native side calls traverse() directly - 'TypeTraverserCallable' => new SmokeTypeTraverserCallable(), - 'array callable' => [$ttMethodCb, 'widenStrings'], - 'first-class callable' => $ttMethodCb->widenStrings(...), - // a traversal started from inside a traversal, which the traverser the - // native side keeps for reuse must not disturb - 'nested map' => static function (\PHPStan\Type\Type $type, callable $traverse) use ($intT): \PHPStan\Type\Type { - if ($type->isString()->yes()) { - return \PHPStan\Type\TypeTraverser::map(new \PHPStan\Type\ArrayType($type, $intT), static fn (\PHPStan\Type\Type $inner, callable $innerTraverse): \PHPStan\Type\Type => $inner->isInteger()->yes() ? new \PHPStan\Type\FloatType() : $innerTraverse($inner)); - } - return $traverse($type); - }, -]; - -foreach ($ttCallbacks as $ttLabel => $ttCallback) { - foreach ($ttInputs as $ttInputLabel => $ttInput) { - $phpResult = \PHPStan\Type\TypeTraverser::map($ttInput, $ttCallback)->describe($ttPrecise); - $nativeResult = \PHPStanTurbo\TypeTraverser::map($ttInput, $ttCallback)->describe($ttPrecise); - check($phpResult === $nativeResult, "TypeTraverser $ttLabel over $ttInputLabel: $phpResult vs $nativeResult"); - } -} - -// an exception from the callback propagates out of map() -foreach (['php' => \PHPStan\Type\TypeTraverser::class, 'native' => \PHPStanTurbo\TypeTraverser::class] as $ttSide => $ttClass) { - $ttThrown = null; - try { - $ttClass::map($ttInputs['array'], static function (\PHPStan\Type\Type $type, callable $traverse): \PHPStan\Type\Type { - throw new \PHPStan\ShouldNotHappenException(); - }); - } catch (\PHPStan\ShouldNotHappenException $e) { - $ttThrown = $e; - } - check($ttThrown !== null, "TypeTraverser $ttSide: the callback's exception propagates"); - - // a callback that returns something else than a Type is a TypeError on - // both sides (the twin's declared return type, checked natively) - $ttTypeError = null; - try { - $ttClass::map($ttInputs['leaf'], static fn (\PHPStan\Type\Type $type, callable $traverse) => 'not a type'); - } catch (TypeError $e) { - $ttTypeError = $e; - } - check($ttTypeError !== null, "TypeTraverser $ttSide: a non-Type callback result is a TypeError"); - - // the traverse callable stays usable after map() returned — the native - // traverser must not recycle an object the callback kept a hold of - $ttEscaped = null; - $ttClass::map($ttInputs['array'], static function (\PHPStan\Type\Type $type, callable $traverse) use (&$ttEscaped): \PHPStan\Type\Type { - $ttEscaped ??= $traverse; - return $traverse($type); - }); - check( - $ttEscaped(new \PHPStan\Type\ArrayType($stringT, $intT))->describe($ttPrecise) === 'array', - "TypeTraverser $ttSide: an escaped traverse callable still works", - ); -} - // ---- ExpressionResultStorage ---- $covered[\PHPStan\Analyser\ExpressionResultStorage::class] = true; $makeScope = static function () {