From 41f4d633bdf9967614b40c16acdd0843a7c282c2 Mon Sep 17 00:00:00 2001 From: Dorian Villet Date: Thu, 2 Jul 2026 19:50:12 +0200 Subject: [PATCH 1/2] Infer array_reduce() callback parameter types from the initial and array arguments Co-Authored-By: Claude Fable 5 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QW8VaDtn3cMdQowPSh1xGG --- .../Helper/ClosureTypeResolver.php | 129 +++++++++ src/Analyser/ScopeOps.php | 8 +- src/Parser/ArrayReduceArgVisitor.php | 48 ++++ tests/PHPStan/Analyser/Bug7280Test.php | 34 +++ .../Analyser/data/bug-7280-unsealed.php | 41 +++ tests/PHPStan/Analyser/nsrt/bug-7280.php | 147 ++++++++++ .../CallToFunctionParametersRuleTest.php | 6 + .../data/array-reduce-carry-concat.php | 16 ++ turbo-ext/src/ArrayReduceArgVisitor.cpp | 129 +++++++++ turbo-ext/src/ClosureTypeResolver.cpp | 259 +++++++++++++++++- .../src/generated/ArrayReduceArgVisitor.h | 44 +++ turbo-ext/src/generated/ClosureTypeResolver.h | 236 +++++++++------- turbo-ext/src/support.cpp | 14 +- turbo-ext/tests/parser-visitors.php | 4 + turbo-ext/tests/smoke.php | 5 + 15 files changed, 1003 insertions(+), 117 deletions(-) create mode 100644 src/Parser/ArrayReduceArgVisitor.php create mode 100644 tests/PHPStan/Analyser/Bug7280Test.php create mode 100644 tests/PHPStan/Analyser/data/bug-7280-unsealed.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-7280.php create mode 100644 tests/PHPStan/Rules/Functions/data/array-reduce-carry-concat.php create mode 100644 turbo-ext/src/ArrayReduceArgVisitor.cpp create mode 100644 turbo-ext/src/generated/ArrayReduceArgVisitor.h diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index 2a519ebcdea..39fe0b22acb 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -25,6 +25,7 @@ use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\PropertyAssignNode; +use PHPStan\Parser\ArrayReduceArgVisitor; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ExtendedParameterReflection; @@ -33,15 +34,18 @@ use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Reflection\Php\DummyParameter; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\ClosureType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NullType; use PHPStan\Type\Type; @@ -150,6 +154,131 @@ public function getClosureType( ); } + // once array_reduce()'s TReturn is resolved, the callback is typed by the + // carry it resolved to, like the walk of its body that put that carry in + $arrayReduceArgs = $expr->getAttribute(ArrayReduceArgVisitor::ATTRIBUTE_NAME); + if ( + $arrayReduceArgs !== null + && ( + $callableParameters === null + || !isset($callableParameters[0]) + || $callableParameters[0]->getType()->hasTemplateOrLateResolvableType() + ) + ) { + return $this->resolveArrayReduceCallbackType($scope, $expr, $storage, $arrayReduceArgs, $parameters, $isVariadic, $contextFree); + } + + return $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, $callableParameters, $nativeCallableParameters, $contextFree); + } + + /** + * Resolves an array_reduce() callback with a bounded fixed-point iteration + * over its $carry parameter. + * + * The first pass types $carry as the initial argument. When the callback's return + * type escapes it, the carry type is widened and verified in a second pass. When + * even the widened type is not a fixed point, the callback is analysed once more + * with an unknown carry instead of claiming a shape the carry can outgrow. + * See isArrayReduceCarryFixedPoint() for what counts as a fixed point. + * + * @param array{Node\Arg, Node\Arg|null} $arrayReduceArgs + * @param list $parameters + */ + private function resolveArrayReduceCallbackType( + MutatingScope $scope, + Node\Expr\Closure|ArrowFunction $expr, + ?ExpressionResultStorage $storage, + array $arrayReduceArgs, + array $parameters, + bool $isVariadic, + bool $contextFree, + ): ClosureType + { + [$arrayArg, $initialArg] = $arrayReduceArgs; + $itemType = $this->readExprType($storage, $arrayArg->value, $scope, false)->getIterableValueType(); + $nativeCallableParameters = self::createArrayReduceCallbackParameters( + new MixedType(), + $this->readExprType($storage, $arrayArg->value, $scope->doNotTreatPhpDocTypesAsCertain(), true)->getIterableValueType(), + ); + + $carryType = $initialArg !== null ? $this->readExprType($storage, $initialArg->value, $scope, false) : new NullType(); + $closureType = $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, self::createArrayReduceCallbackParameters($carryType, $itemType), $nativeCallableParameters, $contextFree); + if (self::isArrayReduceCarryFixedPoint($carryType, $closureType->getReturnType())) { + return $closureType; + } + + $carryType = self::widenArrayReduceCarryType($carryType, $closureType->getReturnType()); + $closureType = $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, self::createArrayReduceCallbackParameters($carryType, $itemType), $nativeCallableParameters, $contextFree); + if (self::isArrayReduceCarryFixedPoint($carryType, $closureType->getReturnType())) { + return $closureType; + } + + return $this->resolveClosureTypeWithParameters($scope, $expr, $parameters, $isVariadic, self::createArrayReduceCallbackParameters(new MixedType(), $itemType), $nativeCallableParameters, $contextFree); + } + + /** + * Whether the callback returning $returnType keeps the carry within $carryType. + * + * Not isSuperTypeOf(): an array shape without definite sealedness is a supertype + * of the same shape with extra keys, so a carry that grows one key per iteration + * would pass. An implicit never return is the inference giving up on the body, + * not a callback that always throws, so it proves nothing about the carry. + */ + private static function isArrayReduceCarryFixedPoint(Type $carryType, Type $returnType): bool + { + if ($returnType instanceof NeverType && !$returnType->isExplicit()) { + return false; + } + + return TypeCombinator::union($carryType, $returnType)->equals($carryType); + } + + /** + * @return list + */ + private static function createArrayReduceCallbackParameters(Type $carryType, Type $itemType): array + { + return [ + new DummyParameter('carry', $carryType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', $itemType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + ]; + } + + /** Widens while preserving constant array shapes so that the second pass can converge. */ + private static function widenArrayReduceCarryType(Type $initialType, Type $returnType): Type + { + $union = TypeCombinator::union($initialType, $returnType); + $constantArrays = $union->getConstantArrays(); + if (count($constantArrays) > 0) { + $generalized = []; + foreach ($constantArrays as $constantArray) { + $generalized[] = $constantArray->generalizeValues(); + } + + return TypeCombinator::union(...$generalized); + } + + return $union->generalize(GeneralizePrecision::lessSpecific()); + } + + /** + * Walks the body with the given callable parameters, or answers from the + * per-context cache. + * + * @param list $parameters + * @param ParameterReflection[]|null $callableParameters + * @param ParameterReflection[]|null $nativeCallableParameters + */ + private function resolveClosureTypeWithParameters( + MutatingScope $scope, + Node\Expr\Closure|ArrowFunction $expr, + array $parameters, + bool $isVariadic, + ?array $callableParameters, + ?array $nativeCallableParameters, + bool $contextFree, + ): ClosureType + { $cachedTypes = $this->findCachedTypes($expr); $cacheKey = $this->closureContextCacheKey($scope, $expr, $callableParameters, $parameters); if (array_key_exists($cacheKey, $cachedTypes)) { diff --git a/src/Analyser/ScopeOps.php b/src/Analyser/ScopeOps.php index 046d1ddc78e..43a25e9f2f3 100644 --- a/src/Analyser/ScopeOps.php +++ b/src/Analyser/ScopeOps.php @@ -15,6 +15,7 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\VirtualNode; use PHPStan\Parser\ArrayMapArgVisitor; +use PHPStan\Parser\ArrayReduceArgVisitor; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; @@ -71,11 +72,14 @@ public static function nodeKey(Expr $node, ExprPrinter $exprPrinter): string $attributes = $node->getAttributes(); if ( $node instanceof Node\FunctionLike - && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + && ( + (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + || (($attributes[ArrayReduceArgVisitor::ATTRIBUTE_NAME] ?? null) !== null) + ) && (($attributes['startFilePos'] ?? null) !== null) ) { $key .= '/*' . $attributes['startFilePos']; - foreach ($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] as $arg) { + foreach ($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? [] as $arg) { $key .= ':' . $exprPrinter->printExpr($arg->value); } $key .= '*/'; diff --git a/src/Parser/ArrayReduceArgVisitor.php b/src/Parser/ArrayReduceArgVisitor.php new file mode 100644 index 00000000000..10b10881de9 --- /dev/null +++ b/src/Parser/ArrayReduceArgVisitor.php @@ -0,0 +1,48 @@ +name instanceof Node\Name && !$node->isFirstClassCallable()) { + $functionName = $node->name->toLowerString(); + if ($functionName === 'array_reduce') { + $arrayArg = null; + $callbackArg = null; + $initialArg = null; + foreach ($node->getArgs() as $i => $arg) { + if ($arg->unpack) { + return null; + } + $name = $arg->name !== null ? $arg->name->toString() : null; + if ($name === 'array' || ($name === null && $i === 0)) { + $arrayArg = $arg; + } elseif ($name === 'callback' || ($name === null && $i === 1)) { + $callbackArg = $arg; + } elseif ($name === 'initial' || ($name === null && $i === 2)) { + $initialArg = $arg; + } + } + if ($arrayArg !== null && $callbackArg !== null) { + $callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, [$arrayArg, $initialArg]); + } + } + } + return null; + } + +} diff --git a/tests/PHPStan/Analyser/Bug7280Test.php b/tests/PHPStan/Analyser/Bug7280Test.php new file mode 100644 index 00000000000..c2f8a77afcf --- /dev/null +++ b/tests/PHPStan/Analyser/Bug7280Test.php @@ -0,0 +1,34 @@ +assertFileAsserts($assertType, $file, ...$args); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-7280-unsealed.php b/tests/PHPStan/Analyser/data/bug-7280-unsealed.php new file mode 100644 index 00000000000..ce133b6b600 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7280-unsealed.php @@ -0,0 +1,41 @@ + $integers + */ +function listAppend(array $integers): void +{ + // the carry grows by one element per item - no array shape is a fixed point + $result = array_reduce($integers, function ($carry, $value) { + $carry[] = $value; + + return $carry; + }, []); + assertType('mixed', $result); + + $typed = array_reduce($integers, function (array $carry, int $value): array { + assertType('array', $carry); + $carry[] = $value; + + return $carry; + }, []); + assertType('array', $typed); +} + +/** + * @param list $strings + */ +function doubleAppend(array $strings): void +{ + $result = array_reduce($strings, function (array $carry, string $value): array { + $carry[] = $value; + $carry[] = $value; + + return $carry; + }, []); + assertType('array', $result); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7280.php b/tests/PHPStan/Analyser/nsrt/bug-7280.php new file mode 100644 index 00000000000..4488101391f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7280.php @@ -0,0 +1,147 @@ += 8.1 + +namespace Bug7280; + +use function PHPStan\Testing\assertType; + +function namedArguments(): void +{ + $result = array_reduce( + ['test1', 'test2'], + static function (array $carry, string $value): array{ + assertType('array{starts: array{}, ends: array{}}|array{starts: non-empty-list, ends: non-empty-list}', $carry); + $carry['starts'][] = $value; + $carry['ends'][] = $value; + + return $carry; + }, + initial: ['starts' => [], 'ends' => []], + ); + assertType('array{starts: non-empty-list, ends: non-empty-list}', $result); +} + +function positionalArguments(): void +{ + $result = array_reduce( + ['test1', 'test2'], + static function (array $carry, string $value): array{ + assertType('array{starts: array{}, ends: array{}}|array{starts: non-empty-list, ends: non-empty-list}', $carry); + $carry['starts'][] = $value; + $carry['ends'][] = $value; + + return $carry; + }, + ['starts' => [], 'ends' => []], + ); + assertType('array{starts: non-empty-list, ends: non-empty-list}', $result); +} + +function reducer(?int $carry, int $item): int +{ + return ($carry ?? 0) + $item; +} + +/** + * @param list $integers + */ +function firstClassCallable(array $integers): void +{ + $result = array_reduce($integers, reducer(...), 0); + assertType('int', $result); +} + +/** + * @param int[] $integers + */ +function nonConvergingCallback(array $integers): void +{ + // the carry type never reaches a fixed point here - the callback is analysed with an unknown carry + $result = array_reduce($integers, function ($carry, $value) { + assertType('array{}|array{x: mixed}', $carry); + + return ['x' => $carry]; + }, []); + assertType('array{}|array{x: array{}|array{x: mixed}}', $result); + + $result2 = array_reduce($integers, fn ($carry, $value) => ['x' => $carry], []); + assertType('array{}|array{x: array{}|array{x: mixed}}', $result2); +} + +/** + * @param array $strings + */ +function arrowFunction(array $strings): void +{ + $result = array_reduce($strings, fn (int $carry, string $value): int => $carry + strlen($value), 0); + assertType('int', $result); +} + +/** + * @param int[] $integers + */ +function intSum(array $integers): void +{ + $sum = array_reduce($integers, function ($carry, $n) { + assertType('int', $carry); + + return $carry + $n; + }, 0); + assertType('int', $sum); +} + +/** + * @param array $strings + */ +function stringConcat(array $strings): void +{ + $concatenated = array_reduce($strings, function (string $carry, string $string): string { + assertType('string', $carry); + + return $carry . $string; + }, ''); + assertType('string', $concatenated); +} + +/** + * @param array $strings + */ +function withoutInitial(array $strings): void +{ + $concatenated = array_reduce($strings, function ($carry, string $string) { + assertType('string|null', $carry); + + return ($carry ?? '') . $string; + }); + assertType('string|null', $concatenated); +} + +/** + * @param list $strings + */ +function countIndexedKeys(array $strings): void +{ + $result = array_reduce($strings, function ($carry, $value) { + $carry['k' . count($carry)] = $value; + + return $carry; + }, []); + assertType('mixed', $result); + + $typed = array_reduce($strings, function (array $carry, string $value): array { + $carry[count($carry)] = $value; + + return $carry; + }, []); + assertType('array', $typed); +} + +/** + * @param list $integers + */ +function alwaysThrows(array $integers): void +{ + $result = array_reduce($integers, function ($carry, $value) { + throw new \Exception(); + }, 0); + assertType('0', $result); +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 3ac6c660706..c7ef3c0205f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -693,6 +693,12 @@ public function testArrayReduceCallback(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testArrayReduceCarryConcat(): void + { + $this->analyse([__DIR__ . '/data/array-reduce-carry-concat.php'], []); + } + public function testArrayReduceArrowFunctionCallback(): void { $this->analyse([__DIR__ . '/data/array_reduce_arrow.php'], [ diff --git a/tests/PHPStan/Rules/Functions/data/array-reduce-carry-concat.php b/tests/PHPStan/Rules/Functions/data/array-reduce-carry-concat.php new file mode 100644 index 00000000000..e7c41d00597 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array-reduce-carry-concat.php @@ -0,0 +1,16 @@ += 8.0 + +namespace ArrayReduceCarryConcat; + +/** + * @param list $integers + */ +function concat(array $integers): void +{ + array_reduce($integers, fn ($carry, $value) => $carry . $value, 'a'); + array_reduce($integers, function ($carry, $value) { + return $carry . $value; + }, 'a'); + array_reduce($integers, fn (string $carry, int $value): string => $carry . $value, '#'); + array_reduce(initial: 'a', callback: fn ($carry, $value) => $carry . $value, array: $integers); +} diff --git a/turbo-ext/src/ArrayReduceArgVisitor.cpp b/turbo-ext/src/ArrayReduceArgVisitor.cpp new file mode 100644 index 00000000000..03e1cf3de46 --- /dev/null +++ b/turbo-ext/src/ArrayReduceArgVisitor.cpp @@ -0,0 +1,129 @@ +/* + * PHPStanTurbo\ArrayReduceArgVisitor — native twin of + * PHPStan\Parser\ArrayReduceArgVisitor, declared under that name at + * activation (final, extending PhpParser\NodeVisitorAbstract like the + * original). + * + * Records the array and initial arguments of an array_reduce() call on its + * callback argument's value. + * + * enterNode() always returns null, so the visitor is also registered with + * pt_native_visitor_register(): the native NodeTraverser then runs it + * directly per node instead of calling into the engine (ParserVisitors.h). + */ + +#include "ParserVisitors.h" +#include "generated/ArrayReduceArgVisitor.h" + +namespace sigs = ptdecl::ArrayReduceArgVisitor::sig; + +static zend_class_entry *pt_ce_array_reduce_arg_visitor = nullptr; + +static const char pt_array_reduce_arg_attribute[] = "arrayReduceArgs"; +static zend_string *pt_array_reduce_arg_attribute_str = nullptr; + +namespace phpstanturbo { + +using visitors::FuncCallProps; +using visitors::NodeProp; + +/* Mirrors PHPStan\Parser\ArrayReduceArgVisitor. */ +class ArrayReduceArgVisitor +{ +public: + /* enterNode(); the twin always returns null, false = pending exception */ + [[nodiscard]] static bool enterNode(zend_object *visitor, zend_object *node) + { + static FuncCallProps call = PT_FUNC_CALL_PROPS; + static NodeProp nameProp = PT_NAME_PROP; + static NodeProp argNameProp = PT_NODE_PROP(PT_CLASS_ARG, "name"); + static NodeProp argValueProp = PT_NODE_PROP(PT_CLASS_ARG, "value"); + static NodeProp argUnpackProp = PT_NODE_PROP(PT_CLASS_ARG, "unpack"); + static NodeProp identifierProp = PT_IDENTIFIER_PROP; + + zval *args = NULL; + zend_string *functionName = visitors::plainFuncCallName(node, call, nameProp, &args); + if (functionName == NULL) return !EG(exception); + if (!visitors::lowerEquals(functionName, "array_reduce")) return true; + + zend_object *arrayArg = NULL; + zend_object *callbackArg = NULL; + zend_object *initialArg = NULL; + for (auto entry : zv::ArrRef(args)) { + zv::Ref arg = entry.value().deref(); + if (!arg.isObject()) continue; + zval *unpack = argUnpackProp.of(arg.asObject()); + if (unpack != NULL && zend_is_true(unpack)) return true; + + /* $arg->name !== null ? $arg->name->toString() : null */ + zend_string *name = NULL; + zval *argName = argNameProp.of(arg.asObject()); + bool named = argName != NULL && Z_TYPE_P(argName) == IS_OBJECT; + if (named) { + name = visitors::nameString(Z_OBJ_P(argName), identifierProp); + } + bool positional = !named && entry.stringKeyOrNull() == NULL; + if ((name != NULL && zend_string_equals_literal(name, "array")) || (positional && entry.indexKey() == 0)) { + arrayArg = arg.asObject(); + } else if ((name != NULL && zend_string_equals_literal(name, "callback")) || (positional && entry.indexKey() == 1)) { + callbackArg = arg.asObject(); + } else if ((name != NULL && zend_string_equals_literal(name, "initial")) || (positional && entry.indexKey() == 2)) { + initialArg = arg.asObject(); + } + } + if (arrayArg == NULL || callbackArg == NULL) return true; + + zval *value = argValueProp.of(callbackArg); + if (value == NULL || Z_TYPE_P(value) != IS_OBJECT) return true; + + /* [$arrayArg, $initialArg] */ + zv::Arr reduceArgs = zv::Arr::create(2); + zval arrayArgZv; + ZVAL_OBJ(&arrayArgZv, arrayArg); + reduceArgs.push(zv::Ref(&arrayArgZv)); + zval initialArgZv; + if (initialArg != NULL) { + ZVAL_OBJ(&initialArgZv, initialArg); + } else { + ZVAL_NULL(&initialArgZv); + } + reduceArgs.push(zv::Ref(&initialArgZv)); + visitors::setAttribute(Z_OBJ_P(value), pt_array_reduce_arg_attribute_str, reduceArgs.raw()); + return true; + } +}; + +} // namespace phpstanturbo + +using phpstanturbo::ArrayReduceArgVisitor; + +/* {{{ engine ABI glue: parameter parsing + registration */ + +static const pt_native_visitor pt_array_reduce_arg_entry = { + &pt_ce_array_reduce_arg_visitor, + ArrayReduceArgVisitor::enterNode, + NULL, + NULL, +}; + +PT_MINIT_REGISTRATION(pt_register_array_reduce_arg_visitor) +{ + pt_array_reduce_arg_attribute_str = zend_string_init_interned(pt_array_reduce_arg_attribute, sizeof(pt_array_reduce_arg_attribute) - 1, 1); + + reg::Class cls("PHPStan\\Parser\\ArrayReduceArgVisitor"); + ptdecl::ArrayReduceArgVisitor::declareClass(cls); + ptdecl::ArrayReduceArgVisitor::declareProperties(cls); + cls.publicClassConstantString("ATTRIBUTE_NAME", pt_array_reduce_arg_attribute); + + cls.method(sigs::enterNode, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *node; + if (!zp::parse(execute_data, node)) RETURN_THROWS(); + if (UNEXPECTED(!ArrayReduceArgVisitor::enterNode(Z_OBJ_P(ZEND_THIS), Z_OBJ_P(node)))) RETURN_THROWS(); + RETURN_NULL(); + }); + + cls.shadow(&pt_ce_array_reduce_arg_visitor); + pt_native_visitor_register(&pt_array_reduce_arg_entry); +} + +/* }}} */ diff --git a/turbo-ext/src/ClosureTypeResolver.cpp b/turbo-ext/src/ClosureTypeResolver.cpp index b1127d440d1..135e09789d9 100644 --- a/turbo-ext/src/ClosureTypeResolver.cpp +++ b/turbo-ext/src/ClosureTypeResolver.cpp @@ -54,6 +54,9 @@ zend_string *pt_ctr_throw_points = nullptr; zend_string *pt_ctr_impure_points = nullptr; zend_string *pt_ctr_invalidate_expressions = nullptr; zend_string *pt_ctr_used_variables = nullptr; +zend_string *pt_ctr_array_reduce_args = nullptr; +zend_string *pt_ctr_carry = nullptr; +zend_string *pt_ctr_item = nullptr; /* the private static $resolveClosureTypeDepth */ int pt_ctr_resolve_closure_type_depth = 0; @@ -64,6 +67,7 @@ int pt_ctr_resolve_closure_type_depth = 0; pt_method_site pt_ctr_simple_throw_point_explicit_site; pt_method_site pt_ctr_simple_throw_point_implicit_site; pt_method_site pt_ctr_stats_increment_site; +pt_method_site pt_ctr_less_specific_site; /* $this->initializerExprTypeResolver->getType($expr, InitializerExprContext::fromScope($scope)) */ zv::Val initializerExprType(zval *initializerExprTypeResolver, zval *expr, zval *scope) @@ -85,6 +89,12 @@ zv::Val simpleThrowPointImplicit() return pt_call_static_cached(pt_ctr_simple_throw_point_implicit_site, PT_CLASS_SIMPLE_THROW_POINT, PT_LC("createimplicit"), 0, NULL); } +/* GeneralizePrecision::lessSpecific() */ +zv::Val generalizePrecisionLessSpecific() +{ + return pt_call_static_cached(pt_ctr_less_specific_site, PT_CLASS_GENERALIZE_PRECISION, PT_LC("lessspecific"), 0, NULL); +} + /* TemplateArgumentStats::$enabled && TemplateArgumentStats::increment('closureTypeBodyWalks'); false = pending exception */ [[nodiscard]] bool countClosureTypeBodyWalk() { @@ -347,21 +357,19 @@ class ClosureTypeResolver // walking its body: parameters plus the DECLARED return type. if (shallow) return signatureClosureType(scope, expr, p.parameters.raw(), p.isVariadic); - zv::Val cachedTypes = findCachedTypes(expr); - zv::Str cacheKey = closureContextCacheKey(scope, expr, p.callableParameters.isNull() ? NULL : p.callableParameters.raw(), p.parameters.raw()); - if (UNEXPECTED(cacheKey.isNull())) return zv::Val(); - zval *cached = zend_symtable_find(Z_ARRVAL_P(cachedTypes.raw()), cacheKey.get()); - if (cached != NULL) { - ZVAL_DEREF(cached); - return createClosureTypeFromCache(scope, expr, p.parameters.raw(), p.isVariadic, cached, p.contextFree); + // once array_reduce()'s TReturn is resolved, the callback is typed by the + // carry it resolved to, like the walk of its body that put that carry in + zval *arrayReduceArgs = ptclosure::attribute(expr, pt_ctr_array_reduce_args); + if (arrayReduceArgs != NULL && Z_TYPE_P(arrayReduceArgs) != IS_NULL) { + bool carryUnresolved; + if (UNEXPECTED(!isArrayReduceCarryUnresolved(p, carryUnresolved))) return zv::Val(); + if (carryUnresolved) { + zv::Val arrayReduceArgsHold = zv::Val::copyOf(zv::Ref(arrayReduceArgs)); + return resolveArrayReduceCallbackType(scope, expr, storage, arrayReduceArgsHold.raw(), p); + } } - if (pt_ctr_resolve_closure_type_depth >= 2) return signatureClosureType(scope, expr, p.parameters.raw(), p.isVariadic); - if (UNEXPECTED(!countClosureTypeBodyWalk())) return zv::Val(); - int isArrowFunction = ptclosure::instanceOf(expr, PT_CLASS_ARROW_FUNCTION); - if (UNEXPECTED(isArrowFunction < 0)) return zv::Val(); - if (isArrowFunction) return walkArrowFunction(scope, expr, p, cacheKey.get()); - return walkClosure(scope, expr, p, cacheKey.get()); + return resolveClosureTypeWithParameters(scope, expr, p); } /* Mirrors buildClosureTypeForClosure() (the nullable ones NULL for null) */ @@ -415,6 +423,228 @@ class ClosureTypeResolver bool contextFree = false; }; + /* Mirrors resolveArrayReduceCallbackType(): the passes replace the + * callable parameters of `p` (its $parameters / $isVariadic / + * $contextFree stay) */ + zv::Val resolveArrayReduceCallbackType(zval *scope, zval *expr, zval *storage, zval *arrayReduceArgs, Parameters &p) + { + // [$arrayArg, $initialArg] = $arrayReduceArgs; + zval null; + ZVAL_NULL(&null); + zval *arrayArg = &null; + zval *initialArg = &null; + if (EXPECTED(Z_TYPE_P(arrayReduceArgs) == IS_ARRAY)) { + zval *found = zend_hash_index_find(Z_ARRVAL_P(arrayReduceArgs), 0); + if (EXPECTED(found != NULL)) { + arrayArg = found; + ZVAL_DEREF(arrayArg); + } + found = zend_hash_index_find(Z_ARRVAL_P(arrayReduceArgs), 1); + if (EXPECTED(found != NULL)) { + initialArg = found; + ZVAL_DEREF(initialArg); + } + } + + zval *arrayValue = ptclosure::prop(ptclosure::argValueSite, arrayArg, PT_LC("value")); + if (UNEXPECTED(arrayValue == NULL)) return zv::Val(); + zv::Val arrayValueHold = zv::Val::copyOf(zv::Ref(arrayValue)); + zv::Val arrayType = readExprType(storage, arrayValueHold.raw(), scope, false); + zv::Val itemType = iterableValueTypeOf(arrayType); + if (UNEXPECTED(itemType.isUndef())) return zv::Val(); + + zv::Val mixedType = pt_type_new_mixed_type(); + if (UNEXPECTED(mixedType.isUndef())) return zv::Val(); + zv::Val nativeScope = pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(scope)); + if (UNEXPECTED(nativeScope.isUndef())) return zv::Val(); + zv::Val nativeArrayType = readExprType(storage, arrayValueHold.raw(), nativeScope.raw(), true); + zv::Val nativeItemType = iterableValueTypeOf(nativeArrayType); + if (UNEXPECTED(nativeItemType.isUndef())) return zv::Val(); + p.nativeCallableParameters = createArrayReduceCallbackParameters(mixedType.raw(), nativeItemType.raw()); + if (UNEXPECTED(p.nativeCallableParameters.isUndef())) return zv::Val(); + + zv::Val carryType; + if (Z_TYPE_P(initialArg) != IS_NULL) { + zval *initialValue = ptclosure::prop(ptclosure::argValueSite, initialArg, PT_LC("value")); + if (UNEXPECTED(initialValue == NULL)) return zv::Val(); + zv::Val initialValueHold = zv::Val::copyOf(zv::Ref(initialValue)); + carryType = readExprType(storage, initialValueHold.raw(), scope, false); + } else { + zval nullType; + if (UNEXPECTED(!pt_null_type_new(&nullType))) return zv::Val(); + carryType = zv::Val::adopt(nullType); + } + if (UNEXPECTED(carryType.isUndef())) return zv::Val(); + + p.callableParameters = createArrayReduceCallbackParameters(carryType.raw(), itemType.raw()); + if (UNEXPECTED(p.callableParameters.isUndef())) return zv::Val(); + zv::Val closureType = resolveClosureTypeWithParameters(scope, expr, p); + zv::Val returnType = closureType.isUndef() ? zv::Val() : returnTypeOf(closureType); + if (UNEXPECTED(returnType.isUndef())) return zv::Val(); + bool isFixedPoint = false; + if (UNEXPECTED(!isArrayReduceCarryFixedPoint(carryType, returnType, isFixedPoint))) return zv::Val(); + if (isFixedPoint) return closureType; + + carryType = widenArrayReduceCarryType(carryType.raw(), returnType.raw()); + if (UNEXPECTED(carryType.isUndef())) return zv::Val(); + p.callableParameters = createArrayReduceCallbackParameters(carryType.raw(), itemType.raw()); + if (UNEXPECTED(p.callableParameters.isUndef())) return zv::Val(); + closureType = resolveClosureTypeWithParameters(scope, expr, p); + returnType = closureType.isUndef() ? zv::Val() : returnTypeOf(closureType); + if (UNEXPECTED(returnType.isUndef())) return zv::Val(); + if (UNEXPECTED(!isArrayReduceCarryFixedPoint(carryType, returnType, isFixedPoint))) return zv::Val(); + if (isFixedPoint) return closureType; + + p.callableParameters = createArrayReduceCallbackParameters(mixedType.raw(), itemType.raw()); + if (UNEXPECTED(p.callableParameters.isUndef())) return zv::Val(); + return resolveClosureTypeWithParameters(scope, expr, p); + } + + /* Mirrors createArrayReduceCallbackParameters(): [carry, item] */ + static zv::Val createArrayReduceCallbackParameters(zval *carryType, zval *itemType) + { + zend_object *no = pt_passed_by_reference_create_no(); + if (UNEXPECTED(no == NULL)) return zv::Val(); + zval noZv; + ZVAL_OBJ(&noZv, no); + zv::Val carry = pt_dummy_parameter_new(pt_ctr_carry, carryType, false, &noZv, false, NULL); + if (UNEXPECTED(carry.isUndef())) return zv::Val(); + zv::Val item = pt_dummy_parameter_new(pt_ctr_item, itemType, false, &noZv, false, NULL); + if (UNEXPECTED(item.isUndef())) return zv::Val(); + zv::Arr parameters = zv::Arr::create(2); + parameters.push(std::move(carry)); + parameters.push(std::move(item)); + return zv::Val(std::move(parameters)); + } + + /* Mirrors widenArrayReduceCarryType() */ + static zv::Val widenArrayReduceCarryType(zval *initialType, zval *returnType) + { + zval types[2]; + ZVAL_COPY_VALUE(&types[0], initialType); + ZVAL_COPY_VALUE(&types[1], returnType); + zv::Val unionType = pt_type_combinator_union(2, types); + if (UNEXPECTED(unionType.isUndef())) return zv::Val(); + if (UNEXPECTED(!unionType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getConstantArrays() on %s", zend_zval_value_name(unionType.raw())); + return zv::Val(); + } + zv::Val constantArrays = pt_type_op(Z_OBJ_P(unionType.raw()), PT_OP_GET_CONSTANT_ARRAYS, 0, NULL); + if (UNEXPECTED(constantArrays.isUndef())) return zv::Val(); + if (EXPECTED(Z_TYPE_P(constantArrays.raw()) == IS_ARRAY) && zend_hash_num_elements(Z_ARRVAL_P(constantArrays.raw())) > 0) { + zv::Arr generalized = zv::Arr::create(zend_hash_num_elements(Z_ARRVAL_P(constantArrays.raw()))); + for (zv::ArrayEntry entry : zv::ArrRef(constantArrays.raw())) { + zval *constantArray = entry.value().deref().raw(); + if (UNEXPECTED(Z_TYPE_P(constantArray) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function generalizeValues() on %s", zend_zval_value_name(constantArray)); + return zv::Val(); + } + zv::Val generalizedArray = pt_type_call(Z_OBJ_P(constantArray), PT_LC("generalizevalues"), 0, NULL); + if (UNEXPECTED(generalizedArray.isUndef())) return zv::Val(); + generalized.push(std::move(generalizedArray)); + } + return ptclosure::unionOf(generalized); + } + + zv::Val precision = generalizePrecisionLessSpecific(); + if (UNEXPECTED(precision.isUndef())) return zv::Val(); + return pt_type_call(Z_OBJ_P(unionType.raw()), PT_LC("generalize"), 1, precision.raw()); + } + + /* $type->getIterableValueType(); UNDEF in, UNDEF out */ + static zv::Val iterableValueTypeOf(zv::Val &type) + { + if (UNEXPECTED(type.isUndef())) return zv::Val(); + if (UNEXPECTED(!type.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getIterableValueType() on %s", zend_zval_value_name(type.raw())); + return zv::Val(); + } + return pt_type_op(Z_OBJ_P(type.raw()), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); + } + + /* $closureType->getReturnType() */ + static zv::Val returnTypeOf(zv::Val &closureType) + { + if (UNEXPECTED(!closureType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function getReturnType() on %s", zend_zval_value_name(closureType.raw())); + return zv::Val(); + } + return pt_type_call(Z_OBJ_P(closureType.raw()), PT_LC("getreturntype"), 0, NULL); + } + + /* getClosureType()'s gate: $callableParameters === null + * || !isset($callableParameters[0]) + * || $callableParameters[0]->getType()->hasTemplateOrLateResolvableType(); + * false = pending exception */ + [[nodiscard]] static bool isArrayReduceCarryUnresolved(Parameters &p, bool &out) + { + out = true; + if (p.callableParameters.isNull() || Z_TYPE_P(p.callableParameters.raw()) != IS_ARRAY) return true; + zval *carry = zend_hash_index_find(Z_ARRVAL_P(p.callableParameters.raw()), 0); + if (carry != NULL) ZVAL_DEREF(carry); + if (carry == NULL || Z_TYPE_P(carry) == IS_NULL) return true; + if (UNEXPECTED(Z_TYPE_P(carry) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getType() on %s", zend_zval_value_name(carry)); + return false; + } + zv::Val carryType = pt_type_call(Z_OBJ_P(carry), PT_LC("gettype"), 0, NULL); + if (UNEXPECTED(carryType.isUndef())) return false; + if (UNEXPECTED(!carryType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function hasTemplateOrLateResolvableType() on %s", zend_zval_value_name(carryType.raw())); + return false; + } + zv::Val result = pt_type_op(Z_OBJ_P(carryType.raw()), PT_OP_HAS_TEMPLATE_OR_LATE_RESOLVABLE_TYPE, 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; + } + + /* Mirrors isArrayReduceCarryFixedPoint(): an implicit never return is no + * fixed point, otherwise TypeCombinator::union($carry, $return)->equals($carry); + * false = pending exception */ + [[nodiscard]] static bool isArrayReduceCarryFixedPoint(zv::Val &carryType, zv::Val &returnType, bool &out) + { + out = false; + if (Z_TYPE_P(returnType.raw()) == IS_OBJECT && instanceof_function(Z_OBJCE_P(returnType.raw()), pt_ce_never_type)) { + zv::Val isExplicit = pt_type_call(Z_OBJ_P(returnType.raw()), PT_LC("isexplicit"), 0, NULL); + if (UNEXPECTED(isExplicit.isUndef())) return false; + if (!zend_is_true(isExplicit.raw())) return true; + } + zval types[2]; + ZVAL_COPY_VALUE(&types[0], carryType.raw()); + ZVAL_COPY_VALUE(&types[1], returnType.raw()); + zv::Val unionType = pt_type_combinator_union(2, types); + if (UNEXPECTED(unionType.isUndef())) return false; + if (UNEXPECTED(!unionType.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function equals() on %s", zend_zval_value_name(unionType.raw())); + return false; + } + zv::Val equal = pt_type_op(Z_OBJ_P(unionType.raw()), PT_OP_EQUALS, 1, carryType.raw()); + if (UNEXPECTED(equal.isUndef())) return false; + out = zend_is_true(equal.raw()); + return true; + } + + /* Mirrors resolveClosureTypeWithParameters() */ + zv::Val resolveClosureTypeWithParameters(zval *scope, zval *expr, Parameters &p) + { + zv::Val cachedTypes = findCachedTypes(expr); + zv::Str cacheKey = closureContextCacheKey(scope, expr, p.callableParameters.isNull() ? NULL : p.callableParameters.raw(), p.parameters.raw()); + if (UNEXPECTED(cacheKey.isNull())) return zv::Val(); + zval *cached = zend_symtable_find(Z_ARRVAL_P(cachedTypes.raw()), cacheKey.get()); + if (cached != NULL) { + ZVAL_DEREF(cached); + return createClosureTypeFromCache(scope, expr, p.parameters.raw(), p.isVariadic, cached, p.contextFree); + } + if (pt_ctr_resolve_closure_type_depth >= 2) return signatureClosureType(scope, expr, p.parameters.raw(), p.isVariadic); + + if (UNEXPECTED(!countClosureTypeBodyWalk())) return zv::Val(); + int isArrowFunction = ptclosure::instanceOf(expr, PT_CLASS_ARROW_FUNCTION); + if (UNEXPECTED(isArrowFunction < 0)) return zv::Val(); + if (isArrowFunction) return walkArrowFunction(scope, expr, p, cacheKey.get()); + return walkClosure(scope, expr, p, cacheKey.get()); + } + void writeSlot(uint32_t index, zv::Val value) { zv::ObjRef(self).propAtWrite(index, std::move(value)); @@ -1561,6 +1791,9 @@ PT_MINIT_REGISTRATION(pt_register_closure_type_resolver) pt_ctr_impure_points = zend_string_init_interned(PT_LC("impurePoints"), 1); pt_ctr_invalidate_expressions = zend_string_init_interned(PT_LC("invalidateExpressions"), 1); pt_ctr_used_variables = zend_string_init_interned(PT_LC("usedVariables"), 1); + pt_ctr_array_reduce_args = zend_string_init_interned(PT_LC("arrayReduceArgs"), 1); + pt_ctr_carry = zend_string_init_interned(PT_LC("carry"), 1); + pt_ctr_item = zend_string_init_interned(PT_LC("item"), 1); reg::Class cls("PHPStan\\Analyser\\ExprHandler\\Helper\\ClosureTypeResolver"); ptdecl::ClosureTypeResolver::declareClass(cls); diff --git a/turbo-ext/src/generated/ArrayReduceArgVisitor.h b/turbo-ext/src/generated/ArrayReduceArgVisitor.h new file mode 100644 index 00000000000..cebbe214014 --- /dev/null +++ b/turbo-ext/src/generated/ArrayReduceArgVisitor.h @@ -0,0 +1,44 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Parser/ArrayReduceArgVisitor.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_ARRAY_REDUCE_ARG_VISITOR_H +#define PHPSTANTURBO_GENERATED_ARRAY_REDUCE_ARG_VISITOR_H + +#include "../reg.h" + +namespace ptdecl::ArrayReduceArgVisitor { + +inline void declareClass(reg::Class &cls) +{ + cls.final(); + cls.parent("PhpParser\\NodeVisitorAbstract"); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + (void) cls; +} + +/* the string and parameter tables the signatures below index into (see reg::Sig) */ +namespace sigtab { +inline constexpr char strings[] = + "node\0" /* 0 */ + "PhpParser\\Node\0" /* 5 */ + "\0" /* 20 */ + "enterNode"; /* 21 */ +inline constexpr reg::PackedArg args[] = { + reg::packed(0, 0, 5), /* enterNode $node */ + reg::packed(20, MAY_BE_NULL, 5), /* enterNode return */ +}; +using Sig = reg::Sig; +} // namespace sigtab + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr sigtab::Sig enterNode = { { 21 /* enterNode */, 1, 0, 1, 1, ZEND_ACC_PUBLIC } }; +} // namespace sig + +} // namespace ptdecl::ArrayReduceArgVisitor + +#endif diff --git a/turbo-ext/src/generated/ClosureTypeResolver.h b/turbo-ext/src/generated/ClosureTypeResolver.h index e1bfec6f1e4..5733130d0fe 100644 --- a/turbo-ext/src/generated/ClosureTypeResolver.h +++ b/turbo-ext/src/generated/ClosureTypeResolver.h @@ -62,40 +62,50 @@ inline constexpr char strings[] = "PHPStan\\Type\\Type\0" /* 549 */ "PHPStan\\Type\\ClosureType\0" /* 567 */ "getClosureType\0" /* 592 */ - "PhpParser\\Node\\Expr\\Closure\0" /* 607 */ - "returnStatements\0" /* 635 */ - "yieldStatements\0" /* 652 */ - "executionEnds\0" /* 668 */ - "throwPoints\0" /* 682 */ - "impurePoints\0" /* 694 */ - "invalidateExpressions\0" /* 707 */ - "native\0" /* 729 */ - "nativePassedToType\0" /* 736 */ - "buildClosureTypeForClosure\0" /* 755 */ - "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 782 */ - "arrowScope\0" /* 816 */ - "buildClosureTypeForArrowFunction\0" /* 827 */ - "callableParameters\0" /* 860 */ - "parameters\0" /* 879 */ - "closureContextCacheKey\0" /* 890 */ - "freeVariableRoots\0" /* 913 */ - "PhpParser\\Node\\Expr\0" /* 931 */ - "readScope\0" /* 951 */ - "readExprType\0" /* 961 */ - "isVariadic\0" /* 974 */ - "cacheKey\0" /* 985 */ - "contextFree\0" /* 994 */ - "buildClosureTypeFromClosureWalk\0" /* 1006 */ - "resolveArrowFunctionReturnType\0" /* 1038 */ - "deriveOnlyNeverExecutionEnds\0" /* 1069 */ - "buildDeclaredParameters\0" /* 1098 */ - "getDeclaredClosureType\0" /* 1122 */ - "buildParametersAndAcceptors\0" /* 1145 */ - "cachedClosureData\0" /* 1173 */ - "createClosureTypeFromCache\0" /* 1191 */ - "returnType\0" /* 1218 */ - "usedVariables\0" /* 1229 */ - "assembleClosureType"; /* 1243 */ + "arrayReduceArgs\0" /* 607 */ + "parameters\0" /* 623 */ + "isVariadic\0" /* 634 */ + "contextFree\0" /* 645 */ + "resolveArrayReduceCallbackType\0" /* 657 */ + "carryType\0" /* 688 */ + "returnType\0" /* 698 */ + "isArrayReduceCarryFixedPoint\0" /* 709 */ + "itemType\0" /* 738 */ + "createArrayReduceCallbackParameters\0" /* 747 */ + "initialType\0" /* 783 */ + "widenArrayReduceCarryType\0" /* 795 */ + "callableParameters\0" /* 821 */ + "nativeCallableParameters\0" /* 840 */ + "resolveClosureTypeWithParameters\0" /* 865 */ + "PhpParser\\Node\\Expr\\Closure\0" /* 898 */ + "returnStatements\0" /* 926 */ + "yieldStatements\0" /* 943 */ + "executionEnds\0" /* 959 */ + "throwPoints\0" /* 973 */ + "impurePoints\0" /* 985 */ + "invalidateExpressions\0" /* 998 */ + "native\0" /* 1020 */ + "nativePassedToType\0" /* 1027 */ + "buildClosureTypeForClosure\0" /* 1046 */ + "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 1073 */ + "arrowScope\0" /* 1107 */ + "buildClosureTypeForArrowFunction\0" /* 1118 */ + "closureContextCacheKey\0" /* 1151 */ + "freeVariableRoots\0" /* 1174 */ + "PhpParser\\Node\\Expr\0" /* 1192 */ + "readScope\0" /* 1212 */ + "readExprType\0" /* 1222 */ + "cacheKey\0" /* 1235 */ + "buildClosureTypeFromClosureWalk\0" /* 1244 */ + "resolveArrowFunctionReturnType\0" /* 1276 */ + "deriveOnlyNeverExecutionEnds\0" /* 1307 */ + "buildDeclaredParameters\0" /* 1336 */ + "getDeclaredClosureType\0" /* 1360 */ + "buildParametersAndAcceptors\0" /* 1383 */ + "cachedClosureData\0" /* 1411 */ + "createClosureTypeFromCache\0" /* 1429 */ + "usedVariables\0" /* 1456 */ + "assembleClosureType"; /* 1470 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 18), /* __construct $nodeScopeResolver */ reg::packed(53, 0, 81), /* __construct $initializerExprTypeResolver */ @@ -110,64 +120,89 @@ inline constexpr reg::PackedArg args[] = { reg::packed(482, MAY_BE_NULL, 490, false, false, 531), /* getClosureType $storage */ reg::packed(536, MAY_BE_NULL, 549, false, false, 531), /* getClosureType $passedToType */ reg::packed(324, 0, 567), /* getClosureType return */ + reg::packed(431, 0, 437), /* resolveArrayReduceCallbackType $scope */ + reg::packed(348, 0, 353), /* resolveArrayReduceCallbackType $expr */ + reg::packed(482, MAY_BE_NULL, 490), /* resolveArrayReduceCallbackType $storage */ + reg::packed(607, MAY_BE_ARRAY), /* resolveArrayReduceCallbackType $arrayReduceArgs */ + reg::packed(623, MAY_BE_ARRAY), /* resolveArrayReduceCallbackType $parameters */ + reg::packed(634, MAY_BE_BOOL), /* resolveArrayReduceCallbackType $isVariadic */ + reg::packed(645, MAY_BE_BOOL), /* resolveArrayReduceCallbackType $contextFree */ + reg::packed(324, 0, 567), /* resolveArrayReduceCallbackType return */ + reg::packed(688, 0, 549), /* isArrayReduceCarryFixedPoint $carryType */ + reg::packed(698, 0, 549), /* isArrayReduceCarryFixedPoint $returnType */ + reg::packed(324, MAY_BE_BOOL), /* isArrayReduceCarryFixedPoint return */ + reg::packed(688, 0, 549), /* createArrayReduceCallbackParameters $carryType */ + reg::packed(738, 0, 549), /* createArrayReduceCallbackParameters $itemType */ + reg::packed(324, MAY_BE_ARRAY), /* createArrayReduceCallbackParameters return */ + reg::packed(783, 0, 549), /* widenArrayReduceCarryType $initialType */ + reg::packed(698, 0, 549), /* widenArrayReduceCarryType $returnType */ + reg::packed(324, 0, 549), /* widenArrayReduceCarryType return */ + reg::packed(431, 0, 437), /* resolveClosureTypeWithParameters $scope */ + reg::packed(348, 0, 353), /* resolveClosureTypeWithParameters $expr */ + reg::packed(623, MAY_BE_ARRAY), /* resolveClosureTypeWithParameters $parameters */ + reg::packed(634, MAY_BE_BOOL), /* resolveClosureTypeWithParameters $isVariadic */ + reg::packed(821, MAY_BE_NULL | MAY_BE_ARRAY), /* resolveClosureTypeWithParameters $callableParameters */ + reg::packed(840, MAY_BE_NULL | MAY_BE_ARRAY), /* resolveClosureTypeWithParameters $nativeCallableParameters */ + reg::packed(645, MAY_BE_BOOL), /* resolveClosureTypeWithParameters $contextFree */ + reg::packed(324, 0, 567), /* resolveClosureTypeWithParameters return */ reg::packed(431, 0, 437), /* buildClosureTypeForClosure $scope */ - reg::packed(348, 0, 607), /* buildClosureTypeForClosure $expr */ - reg::packed(635, MAY_BE_ARRAY), /* buildClosureTypeForClosure $returnStatements */ - reg::packed(652, MAY_BE_ARRAY), /* buildClosureTypeForClosure $yieldStatements */ - reg::packed(668, MAY_BE_ARRAY), /* buildClosureTypeForClosure $executionEnds */ - reg::packed(682, MAY_BE_ARRAY), /* buildClosureTypeForClosure $throwPoints */ - reg::packed(694, MAY_BE_ARRAY), /* buildClosureTypeForClosure $impurePoints */ - reg::packed(707, MAY_BE_ARRAY), /* buildClosureTypeForClosure $invalidateExpressions */ - reg::packed(729, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeForClosure $native */ + reg::packed(348, 0, 898), /* buildClosureTypeForClosure $expr */ + reg::packed(926, MAY_BE_ARRAY), /* buildClosureTypeForClosure $returnStatements */ + reg::packed(943, MAY_BE_ARRAY), /* buildClosureTypeForClosure $yieldStatements */ + reg::packed(959, MAY_BE_ARRAY), /* buildClosureTypeForClosure $executionEnds */ + reg::packed(973, MAY_BE_ARRAY), /* buildClosureTypeForClosure $throwPoints */ + reg::packed(985, MAY_BE_ARRAY), /* buildClosureTypeForClosure $impurePoints */ + reg::packed(998, MAY_BE_ARRAY), /* buildClosureTypeForClosure $invalidateExpressions */ + reg::packed(1020, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeForClosure $native */ reg::packed(482, MAY_BE_NULL, 490, false, false, 531), /* buildClosureTypeForClosure $storage */ reg::packed(536, MAY_BE_NULL, 549, false, false, 531), /* buildClosureTypeForClosure $passedToType */ - reg::packed(736, MAY_BE_NULL, 549, false, false, 531), /* buildClosureTypeForClosure $nativePassedToType */ + reg::packed(1027, MAY_BE_NULL, 549, false, false, 531), /* buildClosureTypeForClosure $nativePassedToType */ reg::packed(324, 0, 567), /* buildClosureTypeForClosure return */ reg::packed(431, 0, 437), /* buildClosureTypeForArrowFunction $scope */ - reg::packed(348, 0, 782), /* buildClosureTypeForArrowFunction $expr */ - reg::packed(816, 0, 437), /* buildClosureTypeForArrowFunction $arrowScope */ - reg::packed(682, MAY_BE_ARRAY), /* buildClosureTypeForArrowFunction $throwPoints */ - reg::packed(694, MAY_BE_ARRAY), /* buildClosureTypeForArrowFunction $impurePoints */ - reg::packed(707, MAY_BE_ARRAY), /* buildClosureTypeForArrowFunction $invalidateExpressions */ - reg::packed(729, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeForArrowFunction $native */ + reg::packed(348, 0, 1073), /* buildClosureTypeForArrowFunction $expr */ + reg::packed(1107, 0, 437), /* buildClosureTypeForArrowFunction $arrowScope */ + reg::packed(973, MAY_BE_ARRAY), /* buildClosureTypeForArrowFunction $throwPoints */ + reg::packed(985, MAY_BE_ARRAY), /* buildClosureTypeForArrowFunction $impurePoints */ + reg::packed(998, MAY_BE_ARRAY), /* buildClosureTypeForArrowFunction $invalidateExpressions */ + reg::packed(1020, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeForArrowFunction $native */ reg::packed(482, MAY_BE_NULL, 490, false, false, 531), /* buildClosureTypeForArrowFunction $storage */ reg::packed(536, MAY_BE_NULL, 549, false, false, 531), /* buildClosureTypeForArrowFunction $passedToType */ - reg::packed(736, MAY_BE_NULL, 549, false, false, 531), /* buildClosureTypeForArrowFunction $nativePassedToType */ + reg::packed(1027, MAY_BE_NULL, 549, false, false, 531), /* buildClosureTypeForArrowFunction $nativePassedToType */ reg::packed(324, 0, 567), /* buildClosureTypeForArrowFunction return */ reg::packed(431, 0, 437), /* closureContextCacheKey $scope */ reg::packed(348, 0, 353), /* closureContextCacheKey $expr */ - reg::packed(860, MAY_BE_NULL | MAY_BE_ARRAY), /* closureContextCacheKey $callableParameters */ - reg::packed(879, MAY_BE_ARRAY), /* closureContextCacheKey $parameters */ + reg::packed(821, MAY_BE_NULL | MAY_BE_ARRAY), /* closureContextCacheKey $callableParameters */ + reg::packed(623, MAY_BE_ARRAY), /* closureContextCacheKey $parameters */ reg::packed(324, MAY_BE_STRING), /* closureContextCacheKey return */ reg::packed(348, 0, 353), /* freeVariableRoots $expr */ reg::packed(324, MAY_BE_NULL | MAY_BE_ARRAY), /* freeVariableRoots return */ reg::packed(482, MAY_BE_NULL, 490), /* readExprType $storage */ - reg::packed(348, 0, 931), /* readExprType $expr */ - reg::packed(951, 0, 437), /* readExprType $readScope */ - reg::packed(729, MAY_BE_BOOL), /* readExprType $native */ + reg::packed(348, 0, 1192), /* readExprType $expr */ + reg::packed(1212, 0, 437), /* readExprType $readScope */ + reg::packed(1020, MAY_BE_BOOL), /* readExprType $native */ reg::packed(324, 0, 549), /* readExprType return */ reg::packed(431, 0, 437), /* buildClosureTypeFromClosureWalk $scope */ - reg::packed(348, 0, 607), /* buildClosureTypeFromClosureWalk $expr */ - reg::packed(879, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $parameters */ - reg::packed(974, MAY_BE_BOOL), /* buildClosureTypeFromClosureWalk $isVariadic */ - reg::packed(635, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $returnStatements */ - reg::packed(652, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $yieldStatements */ - reg::packed(668, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $executionEnds */ - reg::packed(682, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $throwPoints */ - reg::packed(694, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $impurePoints */ - reg::packed(707, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $invalidateExpressions */ - reg::packed(985, MAY_BE_NULL | MAY_BE_STRING, reg::NoString, false, false, 531), /* buildClosureTypeFromClosureWalk $cacheKey */ - reg::packed(729, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeFromClosureWalk $native */ + reg::packed(348, 0, 898), /* buildClosureTypeFromClosureWalk $expr */ + reg::packed(623, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $parameters */ + reg::packed(634, MAY_BE_BOOL), /* buildClosureTypeFromClosureWalk $isVariadic */ + reg::packed(926, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $returnStatements */ + reg::packed(943, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $yieldStatements */ + reg::packed(959, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $executionEnds */ + reg::packed(973, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $throwPoints */ + reg::packed(985, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $impurePoints */ + reg::packed(998, MAY_BE_ARRAY), /* buildClosureTypeFromClosureWalk $invalidateExpressions */ + reg::packed(1235, MAY_BE_NULL | MAY_BE_STRING, reg::NoString, false, false, 531), /* buildClosureTypeFromClosureWalk $cacheKey */ + reg::packed(1020, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeFromClosureWalk $native */ reg::packed(482, MAY_BE_NULL, 490, false, false, 531), /* buildClosureTypeFromClosureWalk $storage */ - reg::packed(994, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeFromClosureWalk $contextFree */ + reg::packed(645, MAY_BE_BOOL, reg::NoString, false, false, 476), /* buildClosureTypeFromClosureWalk $contextFree */ reg::packed(324, 0, 567), /* buildClosureTypeFromClosureWalk return */ reg::packed(431, 0, 437), /* resolveArrowFunctionReturnType $scope */ - reg::packed(816, 0, 437), /* resolveArrowFunctionReturnType $arrowScope */ - reg::packed(348, 0, 782), /* resolveArrowFunctionReturnType $expr */ - reg::packed(729, MAY_BE_BOOL, reg::NoString, false, false, 476), /* resolveArrowFunctionReturnType $native */ + reg::packed(1107, 0, 437), /* resolveArrowFunctionReturnType $arrowScope */ + reg::packed(348, 0, 1073), /* resolveArrowFunctionReturnType $expr */ + reg::packed(1020, MAY_BE_BOOL, reg::NoString, false, false, 476), /* resolveArrowFunctionReturnType $native */ reg::packed(482, MAY_BE_NULL, 490, false, false, 531), /* resolveArrowFunctionReturnType $storage */ reg::packed(324, 0, 549), /* resolveArrowFunctionReturnType return */ - reg::packed(668, MAY_BE_ARRAY), /* deriveOnlyNeverExecutionEnds $executionEnds */ + reg::packed(959, MAY_BE_ARRAY), /* deriveOnlyNeverExecutionEnds $executionEnds */ reg::packed(324, MAY_BE_NULL | MAY_BE_BOOL), /* deriveOnlyNeverExecutionEnds return */ reg::packed(431, 0, 437), /* buildDeclaredParameters $scope */ reg::packed(348, 0, 353), /* buildDeclaredParameters $expr */ @@ -179,26 +214,26 @@ inline constexpr reg::PackedArg args[] = { reg::packed(348, 0, 353), /* buildParametersAndAcceptors $expr */ reg::packed(482, MAY_BE_NULL, 490, false, false, 531), /* buildParametersAndAcceptors $storage */ reg::packed(536, MAY_BE_NULL, 549, false, false, 531), /* buildParametersAndAcceptors $passedToType */ - reg::packed(736, MAY_BE_NULL, 549, false, false, 531), /* buildParametersAndAcceptors $nativePassedToType */ + reg::packed(1027, MAY_BE_NULL, 549, false, false, 531), /* buildParametersAndAcceptors $nativePassedToType */ reg::packed(324, MAY_BE_ARRAY), /* buildParametersAndAcceptors return */ reg::packed(431, 0, 437), /* createClosureTypeFromCache $scope */ reg::packed(348, 0, 353), /* createClosureTypeFromCache $expr */ - reg::packed(879, MAY_BE_ARRAY), /* createClosureTypeFromCache $parameters */ - reg::packed(974, MAY_BE_BOOL), /* createClosureTypeFromCache $isVariadic */ - reg::packed(1173, MAY_BE_ARRAY), /* createClosureTypeFromCache $cachedClosureData */ - reg::packed(994, MAY_BE_BOOL), /* createClosureTypeFromCache $contextFree */ + reg::packed(623, MAY_BE_ARRAY), /* createClosureTypeFromCache $parameters */ + reg::packed(634, MAY_BE_BOOL), /* createClosureTypeFromCache $isVariadic */ + reg::packed(1411, MAY_BE_ARRAY), /* createClosureTypeFromCache $cachedClosureData */ + reg::packed(645, MAY_BE_BOOL), /* createClosureTypeFromCache $contextFree */ reg::packed(324, 0, 567), /* createClosureTypeFromCache return */ reg::packed(431, 0, 437), /* assembleClosureType $scope */ reg::packed(348, 0, 353), /* assembleClosureType $expr */ - reg::packed(879, MAY_BE_ARRAY), /* assembleClosureType $parameters */ - reg::packed(974, MAY_BE_BOOL), /* assembleClosureType $isVariadic */ - reg::packed(1218, 0, 549), /* assembleClosureType $returnType */ - reg::packed(682, MAY_BE_ARRAY), /* assembleClosureType $throwPoints */ - reg::packed(694, MAY_BE_ARRAY), /* assembleClosureType $impurePoints */ - reg::packed(707, MAY_BE_ARRAY), /* assembleClosureType $invalidateExpressions */ - reg::packed(1229, MAY_BE_ARRAY), /* assembleClosureType $usedVariables */ - reg::packed(985, MAY_BE_NULL | MAY_BE_STRING, reg::NoString, false, false, 531), /* assembleClosureType $cacheKey */ - reg::packed(994, MAY_BE_BOOL, reg::NoString, false, false, 476), /* assembleClosureType $contextFree */ + reg::packed(623, MAY_BE_ARRAY), /* assembleClosureType $parameters */ + reg::packed(634, MAY_BE_BOOL), /* assembleClosureType $isVariadic */ + reg::packed(698, 0, 549), /* assembleClosureType $returnType */ + reg::packed(973, MAY_BE_ARRAY), /* assembleClosureType $throwPoints */ + reg::packed(985, MAY_BE_ARRAY), /* assembleClosureType $impurePoints */ + reg::packed(998, MAY_BE_ARRAY), /* assembleClosureType $invalidateExpressions */ + reg::packed(1456, MAY_BE_ARRAY), /* assembleClosureType $usedVariables */ + reg::packed(1235, MAY_BE_NULL | MAY_BE_STRING, reg::NoString, false, false, 531), /* assembleClosureType $cacheKey */ + reg::packed(645, MAY_BE_BOOL, reg::NoString, false, false, 476), /* assembleClosureType $contextFree */ reg::packed(324, 0, 567), /* assembleClosureType return */ }; using Sig = reg::Sig; @@ -210,19 +245,24 @@ inline constexpr sigtab::Sig __construct = { { 312 /* __construct */, 4, 0, 4, r inline constexpr sigtab::Sig resetFileAnalysisState = { { 325 /* resetFileAnalysisState */, 0, 4, 0, 4, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig findCachedTypes = { { 415 /* findCachedTypes */, 1, 5, 1, 6, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig getClosureType = { { 592 /* getClosureType */, 2, 7, 5, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig buildClosureTypeForClosure = { { 755 /* buildClosureTypeForClosure */, 8, 13, 12, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig buildClosureTypeForArrowFunction = { { 827 /* buildClosureTypeForArrowFunction */, 6, 26, 10, 36, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig closureContextCacheKey = { { 890 /* closureContextCacheKey */, 4, 37, 4, 41, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig freeVariableRoots = { { 913 /* freeVariableRoots */, 1, 42, 1, 43, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig readExprType = { { 961 /* readExprType */, 4, 44, 4, 48, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig buildClosureTypeFromClosureWalk = { { 1006 /* buildClosureTypeFromClosureWalk */, 10, 49, 14, 63, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveArrowFunctionReturnType = { { 1038 /* resolveArrowFunctionReturnType */, 3, 64, 5, 69, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig deriveOnlyNeverExecutionEnds = { { 1069 /* deriveOnlyNeverExecutionEnds */, 1, 70, 1, 71, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig buildDeclaredParameters = { { 1098 /* buildDeclaredParameters */, 2, 72, 2, 74, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getDeclaredClosureType = { { 1122 /* getDeclaredClosureType */, 2, 75, 2, 77, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig buildParametersAndAcceptors = { { 1145 /* buildParametersAndAcceptors */, 2, 78, 5, 83, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig createClosureTypeFromCache = { { 1191 /* createClosureTypeFromCache */, 6, 84, 6, 90, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig assembleClosureType = { { 1243 /* assembleClosureType */, 9, 91, 11, 102, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveArrayReduceCallbackType = { { 657 /* resolveArrayReduceCallbackType */, 7, 13, 7, 20, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isArrayReduceCarryFixedPoint = { { 709 /* isArrayReduceCarryFixedPoint */, 2, 21, 2, 23, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig createArrayReduceCallbackParameters = { { 747 /* createArrayReduceCallbackParameters */, 2, 24, 2, 26, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig widenArrayReduceCarryType = { { 795 /* widenArrayReduceCarryType */, 2, 27, 2, 29, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig resolveClosureTypeWithParameters = { { 865 /* resolveClosureTypeWithParameters */, 7, 30, 7, 37, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig buildClosureTypeForClosure = { { 1046 /* buildClosureTypeForClosure */, 8, 38, 12, 50, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig buildClosureTypeForArrowFunction = { { 1118 /* buildClosureTypeForArrowFunction */, 6, 51, 10, 61, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig closureContextCacheKey = { { 1151 /* closureContextCacheKey */, 4, 62, 4, 66, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig freeVariableRoots = { { 1174 /* freeVariableRoots */, 1, 67, 1, 68, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig readExprType = { { 1222 /* readExprType */, 4, 69, 4, 73, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig buildClosureTypeFromClosureWalk = { { 1244 /* buildClosureTypeFromClosureWalk */, 10, 74, 14, 88, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveArrowFunctionReturnType = { { 1276 /* resolveArrowFunctionReturnType */, 3, 89, 5, 94, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig deriveOnlyNeverExecutionEnds = { { 1307 /* deriveOnlyNeverExecutionEnds */, 1, 95, 1, 96, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig buildDeclaredParameters = { { 1336 /* buildDeclaredParameters */, 2, 97, 2, 99, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getDeclaredClosureType = { { 1360 /* getDeclaredClosureType */, 2, 100, 2, 102, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig buildParametersAndAcceptors = { { 1383 /* buildParametersAndAcceptors */, 2, 103, 5, 108, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig createClosureTypeFromCache = { { 1429 /* createClosureTypeFromCache */, 6, 109, 6, 115, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig assembleClosureType = { { 1470 /* assembleClosureType */, 9, 116, 11, 127, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::ClosureTypeResolver diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index f3eaca418cd..a8b22826da6 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -503,6 +503,8 @@ zend_string *pt_str_cache_printer = nullptr; zend_string *pt_str_contains_super_global = nullptr; zend_string *pt_str_contains_call = nullptr; zend_string *pt_str_array_map_args = nullptr; +/* ArrayReduceArgVisitor::ATTRIBUTE_NAME, read by pt_node_key() only */ +static zend_string *pt_str_array_reduce_args = nullptr; zend_string *pt_str_start_file_pos = nullptr; zend_string *pt_str_end_file_pos = nullptr; static bool pt_strs_inited = false; @@ -518,6 +520,7 @@ void pt_init_strs() pt_str_contains_super_global = zend_string_init("containsSuperGlobal", sizeof("containsSuperGlobal") - 1, 0); pt_str_contains_call = zend_string_init("containsCall", sizeof("containsCall") - 1, 0); pt_str_array_map_args = zend_string_init("arrayMapArgs", sizeof("arrayMapArgs") - 1, 0); + pt_str_array_reduce_args = zend_string_init("arrayReduceArgs", sizeof("arrayReduceArgs") - 1, 0); pt_str_start_file_pos = zend_string_init("startFilePos", sizeof("startFilePos") - 1, 0); pt_str_end_file_pos = zend_string_init("endFilePos", sizeof("endFilePos") - 1, 0); pt_strs_inited = true; @@ -562,6 +565,7 @@ void pt_support_rshutdown() zend_string_release(pt_str_contains_super_global); zend_string_release(pt_str_contains_call); zend_string_release(pt_str_array_map_args); + zend_string_release(pt_str_array_reduce_args); zend_string_release(pt_str_start_file_pos); zend_string_release(pt_str_end_file_pos); pt_strs_inited = false; @@ -960,8 +964,9 @@ zend_string *pt_node_key(zend_object *node, zval *expr_printer) key = pt_node_printed_expr(node, expr_printer); if (key == NULL) return NULL; - /* FunctionLike with arrayMapArgs + startFilePos: append the array_map - * argument suffix exactly like MutatingScope::getNodeKey() */ + /* FunctionLike with arrayMapArgs or arrayReduceArgs + startFilePos: append + * the position (and the array_map arguments) exactly like + * ScopeOps::nodeKey() */ { zend_class_entry *fl_ce = pt_class(PT_CLASS_FUNCTION_LIKE); if (fl_ce == NULL) { @@ -970,8 +975,9 @@ zend_string *pt_node_key(zend_object *node, zval *expr_printer) } if (instanceof_function(node->ce, fl_ce)) { zval *map_args = pt_node_attribute(node, pt_str_array_map_args); + zval *reduce_args = pt_node_attribute(node, pt_str_array_reduce_args); zval *start_pos = pt_node_attribute(node, pt_str_start_file_pos); - if (map_args != NULL && Z_TYPE_P(map_args) != IS_NULL + if (((map_args != NULL && Z_TYPE_P(map_args) != IS_NULL) || (reduce_args != NULL && Z_TYPE_P(reduce_args) != IS_NULL)) && start_pos != NULL && Z_TYPE_P(start_pos) != IS_NULL) { smart_str str = {}; smart_str_append(&str, key); @@ -979,7 +985,7 @@ zend_string *pt_node_key(zend_object *node, zval *expr_printer) if (Z_TYPE_P(start_pos) == IS_LONG) { smart_str_append_long(&str, Z_LVAL_P(start_pos)); } - if (Z_TYPE_P(map_args) == IS_ARRAY) { + if (map_args != NULL && Z_TYPE_P(map_args) == IS_ARRAY) { zval *arg; ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(map_args), arg) { zval *arg_deref = arg; diff --git a/turbo-ext/tests/parser-visitors.php b/turbo-ext/tests/parser-visitors.php index e89ef34580d..95acea32f26 100644 --- a/turbo-ext/tests/parser-visitors.php +++ b/turbo-ext/tests/parser-visitors.php @@ -25,6 +25,7 @@ 'ArrayFindArgVisitor', 'ArrayMapArgVisitor', 'ArrayOffsetNormalizingVisitor', + 'ArrayReduceArgVisitor', 'ArrayWalkArgVisitor', 'ArrowFunctionArgVisitor', 'ClosureArgVisitor', @@ -66,6 +67,9 @@ 'array_reduce($a, $cb, 1); array_reduce($a, $cb, 1, 2);', // closures and arrow functions called where they are written 'setAttribute(\PHPStan\Parser\ArrayMapArgVisitor::ATTRIBUTE_NAME, [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('items'))]); $arrayMapClosure->setAttribute('startFilePos', 123); +$arrayReduceClosure = new \PhpParser\Node\Expr\ArrowFunction(['expr' => new \PhpParser\Node\Expr\Variable('carry')]); +$arrayReduceClosure->setAttribute(\PHPStan\Parser\ArrayReduceArgVisitor::ATTRIBUTE_NAME, [new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Variable('items')), null]); +$arrayReduceClosure->setAttribute('startFilePos', 456); $nodeKeyCases = [ 'variable fast path' => new \PhpParser\Node\Expr\Variable('foo'), 'variable variable' => new \PhpParser\Node\Expr\Variable(new \PhpParser\Node\Expr\Variable('foo')), 'method call' => new \PhpParser\Node\Expr\MethodCall(new \PhpParser\Node\Expr\Variable('o'), 'm', [new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\Int_(1))]), 'array_map closure' => $arrayMapClosure, + 'array_reduce arrow function' => $arrayReduceClosure, ]; foreach ($nodeKeyCases as $label => $nodeKeyExpr) { check( From 87a2f4ea9ce558d8a6c6e70d753e663f13676eb9 Mon Sep 17 00:00:00 2001 From: Dorian Villet Date: Mon, 12 Oct 2026 20:49:07 +0200 Subject: [PATCH 2/2] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 88c83cac563..3959d4f47ac 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '28c29b8'; + public const EXPECTED_EXTENSION_VERSION = '41f4d63'; private static bool $active = false;