diff --git a/src/Analyser/ArgumentsHandler.php b/src/Analyser/ArgumentsHandler.php index 905cc628f9b..ce48827a517 100644 --- a/src/Analyser/ArgumentsHandler.php +++ b/src/Analyser/ArgumentsHandler.php @@ -71,6 +71,7 @@ use function array_values; use function count; use function get_class; +use function is_array; use function is_string; use function spl_object_id; use function sprintf; @@ -1001,11 +1002,13 @@ private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $ * built without walking anything: a nested array literal recurses, a closure / * arrow function contributes its DECLARED signature * (ClosureTypeResolver::getDeclaredClosureType()), and every other key/value - * is priced by the scope state it is already tracked as, falling back to the + * is priced by the scope state it is already tracked as, while preceding + * elements have not changed that state, falling back to the * constant-expression resolver (literals, ::class, constants, concatenation) - * and ultimately to mixed. Widening a slot to mixed is safe: a template is - * never resolved below its bound, so the skeleton can only ever sharpen the - * resolution. + * and ultimately to mixed. Scope-known method callables retain their variants; + * expressions that require a walk are mixed. Widening a slot to mixed is + * safe: a template is never resolved below its bound, so the skeleton can + * only ever sharpen the resolution. * * Unlike gatherClosureArgType() this type never reaches $gatheredTypes: it * exists solely to resolve the parameter type the nested closures are typed @@ -1014,20 +1017,82 @@ private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $ private function gatherArrayArgTypeSkeleton(NodeScopeResolver $nodeScopeResolver, Expr\Array_ $expr, MutatingScope $scope): Type { $initializerContext = InitializerExprContext::fromScope($scope); - $getType = function (Expr $inner) use (&$getType, $nodeScopeResolver, $scope, $initializerContext): Type { + $changesScope = static function (Node $node) use (&$changesScope): bool { + if ($node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction) { + return false; + } + if ( + ($node instanceof CallLike && !$node->isFirstClassCallable()) + || $node instanceof Expr\Assign || $node instanceof Expr\AssignRef || $node instanceof Expr\AssignOp + || $node instanceof Expr\PreInc || $node instanceof Expr\PreDec || $node instanceof Expr\PostInc || $node instanceof Expr\PostDec + || $node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom || $node instanceof Expr\Include_ || $node instanceof Expr\Eval_ + ) { + return true; + } + foreach ($node->getSubNodeNames() as $name) { + $children = $node->{$name}; + foreach (is_array($children) ? $children : [$children] as $child) { + if ($child instanceof Node && $changesScope($child)) { + return true; + } + } + } + return false; + }; + $types = []; + $scopeIsValid = true; + $getType = function (Expr $inner) use (&$getType, &$types, &$scopeIsValid, $changesScope, $nodeScopeResolver, $scope, $initializerContext): Type { if ($inner instanceof Expr\Closure || $inner instanceof Expr\ArrowFunction) { return $this->closureTypeResolver->getDeclaredClosureType($scope, $inner); } if ($inner instanceof Expr\Array_) { - return $this->initializerExprTypeResolver->getArrayType($inner, $getType); + // Price keys before values, as in the real walk. The array builder + // requests values first, so give it the already computed types. + foreach ($inner->items as $item) { + if ($item->key !== null) { + $types[spl_object_id($item->key)] = $getType($item->key); + } + $types[spl_object_id($item->value)] = $getType($item->value); + if (!$item->byRef) { + continue; + } + + $scopeIsValid = false; + } + return $this->initializerExprTypeResolver->getArrayType($inner, static fn (Expr $item): Type => $types[spl_object_id($item)]); } - return $nodeScopeResolver->findScopeStateType($inner, $scope) - ?? $this->initializerExprTypeResolver->getType($inner, $initializerContext); + if ($changesScope($inner)) { + $scopeIsValid = false; + return new MixedType(); + } + if ($scopeIsValid) { + $stateType = $nodeScopeResolver->findScopeStateType($inner, $scope); + if ($stateType !== null) { + return $stateType; + } + if ($inner instanceof MethodCall && $inner->isFirstClassCallable() && $inner->name instanceof Identifier) { + $receiverType = $nodeScopeResolver->findScopeStateType($inner->var, $scope); + if ($receiverType !== null) { + $method = $scope->getMethodReflection($receiverType, $inner->name->toString()); + if ($method !== null) { + return $this->initializerExprTypeResolver->createFirstClassCallable($method, $method->getVariants(), $scope->nativeTypesPromoted); + } + } + } + } + if ( + !($inner instanceof FuncCall && $inner->name instanceof Name) + && !($inner instanceof StaticCall && $inner->class instanceof Name && $inner->name instanceof Identifier) + && (new NodeFinder())->findFirst($inner, static fn (Node $node): bool => $node instanceof CallLike) !== null + ) { + return new MixedType(); + } + return $this->initializerExprTypeResolver->getType($inner, $initializerContext); }; - return $this->initializerExprTypeResolver->getArrayType($expr, $getType); + return $getType($expr); } /** diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 88c83cac563..7fa5da11f40 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 = 'f267b22'; private static bool $active = false; diff --git a/tests/PHPStan/Analyser/ArrayArgumentSkeletonTest.php b/tests/PHPStan/Analyser/ArrayArgumentSkeletonTest.php new file mode 100644 index 00000000000..fe9c52cdb1d --- /dev/null +++ b/tests/PHPStan/Analyser/ArrayArgumentSkeletonTest.php @@ -0,0 +1,32 @@ += 8.1.0')] + public function testAsserts(string $assertType, string $file, ...$args): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-15432-array-effects.php b/tests/PHPStan/Analyser/nsrt/bug-15432-array-effects.php new file mode 100644 index 00000000000..3e81c3836f1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15432-array-effects.php @@ -0,0 +1,104 @@ += 8.1 + +namespace Bug15432ArrayEffects; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @param array{first: mixed, value: T, callback: callable(T): void} $spec + */ +function run(array $spec): void {} + +function increment(): void +{ + $i = 0; + run([ + 'first' => $i++, + 'value' => $i, + 'callback' => function ($value): void { assertType('mixed', $value); }, + ]); +} + +function assignment(): void +{ + $value = 1; + run([ + 'first' => $value = 'str', + 'value' => $value, + 'callback' => function ($value): void { assertType('mixed', $value); }, + ]); +} + +function nested(): void +{ + $i = 0; + run([ + 'first' => [$i++], + 'value' => $i, + 'callback' => function ($value): void { assertType('mixed', $value); }, + ]); +} + +function mutate(int &$value): void { $value++; } + +function byReferenceArgument(): void +{ + $i = 0; + run([ + 'first' => mutate($i), + 'value' => $i, + 'callback' => function ($value): void { assertType('mixed', $value); }, + ]); +} + +function keyEffect(): void +{ + $i = 0; + run([ + ($i++ === 0 ? 'first' : 'first') => $i, + 'value' => $i, + 'callback' => function ($value): void { assertType('mixed', $value); }, + ]); +} + +function literalAfterEffect(): void +{ + $i = 0; + run([ + 'first' => $i++, + 'value' => 42, + 'callback' => function ($value): void { assertType('42', $value); }, + ]); +} + +class Impure +{ + + public int $prop = 5; + + public function reset(): void { $this->prop = 7; } + + public function test(): void + { + if ($this->prop !== 5) { + return; + } + run([ + 'first' => $this->reset(), + 'value' => $this->prop, + 'callback' => function ($value): void { assertType('mixed', $value); }, + ]); + } + +} + +function deferredBody(): void +{ + $i = 0; + run([ + 'first' => function () use (&$i): void { $i++; }, + 'value' => $i, + 'callback' => function ($value): void { assertType('0', $value); }, + ]); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-15432-callable.php b/tests/PHPStan/Analyser/nsrt/bug-15432-callable.php new file mode 100644 index 00000000000..37223779f95 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15432-callable.php @@ -0,0 +1,52 @@ += 8.1 + +namespace Bug15432Callable; + +use function PHPStan\Testing\assertType; + +/** + * @template T + * @param array{producer: callable(): T, consumer: callable(T): void} $pair + */ +function pipe(array $pair): void {} + +class Producer +{ + + public function produce(): int { return 1; } + public static function produceStatic(): int { return 1; } + +} + +function test(Producer $producer): void +{ + pipe([ + 'producer' => $producer->produce(...), + 'consumer' => function ($value): void { assertType('int', $value); }, + ]); + $callable = $producer->produce(...); + pipe([ + 'producer' => $callable, + 'consumer' => function ($value): void { assertType('int', $value); }, + ]); + pipe([ + 'producer' => Producer::produceStatic(...), + 'consumer' => function ($value): void { assertType('int', $value); }, + ]); +} + +function dynamicName(Producer $producer, string $name): void +{ + pipe([ + 'producer' => $producer->{$name}(...), + 'consumer' => function ($value): void { assertType('mixed', $value); }, + ]); +} + +function dynamicStatic(string $className, string $methodName): void +{ + pipe([ + 'producer' => $className::{$methodName}(...), + 'consumer' => function ($value): void { assertType('mixed', $value); }, + ]); +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 3ac6c660706..d087f285103 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1230,6 +1230,21 @@ public function testFirstClassCallables(): void $this->analyse([__DIR__ . '/data/first-class-callables.php'], []); } + public function testBug15432(): void + { + $this->analyse([__DIR__ . '/data/bug-15432.php'], []); + } + + public function testBug15432Invalid(): void + { + $this->analyse([__DIR__ . '/data/bug-15432-invalid.php'], [ + [ + 'Parameter #1 $callbacks of function Bug15432Invalid\\consume expects array<\'closure\'|\'method\', callable(int): string>, array{method: Closure(string): int, closure: Closure(int): decimal-int-string} given.', + 23, + ], + ]); + } + public function testBug4413(): void { require_once __DIR__ . '/data/bug-4413.php'; diff --git a/tests/PHPStan/Rules/Functions/data/bug-15432-invalid.php b/tests/PHPStan/Rules/Functions/data/bug-15432-invalid.php new file mode 100644 index 00000000000..f1bf04b35b7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15432-invalid.php @@ -0,0 +1,27 @@ += 8.1 + +namespace Bug15432Invalid; + +/** + * @template T of string + * @param array $callbacks + */ +function consume(array $callbacks): void +{ +} + +class Handler +{ + public function handle(string $value): int + { + return strlen($value); + } +} + +function invalid(Handler $handler): void +{ + consume([ + 'method' => $handler->handle(...), + 'closure' => static fn (int $value): string => (string) $value, + ]); +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-15432.php b/tests/PHPStan/Rules/Functions/data/bug-15432.php new file mode 100644 index 00000000000..f8919318507 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15432.php @@ -0,0 +1,79 @@ += 8.1 + +namespace Bug15432; + +/** + * @template T of string + * @param array $callbacks + */ +function consume(array $callbacks): void +{ + foreach ($callbacks as $handler) { + $handler(); + } +} + +consume([ + 'method' => (new class { + public function handle(): void {} + })->handle(...), + 'closure' => function (): void {}, +]); + +class Handler +{ + public function handle(): void {} +} + +function instanceMethods(Handler $handler): void +{ + consume([ + 'method' => $handler->handle(...), + 'closure' => static function (): void {}, + ]); + consume([ + 'method' => $handler->handle(...), + 'closure' => fn () => null, + ]); + $callable = $handler->handle(...); + consume([ + 'method' => $callable, + 'closure' => static function (): void {}, + ]); +} + +/** + * @template T of string + * @param array> $callbacks + */ +function consumeNested(array $callbacks): void +{ +} + +function nested(Handler $handler): void +{ + consumeNested([ + 'callbacks' => [ + 'method' => $handler->handle(...), + 'closure' => static function (): void {}, + ], + ]); +} + +/** + * @template T of string + * @param array $callbacks + */ +function consumeAny(array $callbacks): void {} + +function expressionCallables(\Closure $callback): void +{ + consumeAny([ + 'method' => $callback(...), + 'closure' => static function (): void {}, + ]); + consumeAny([ + 'method' => 'strlen'(...), + 'closure' => static function (): void {}, + ]); +} diff --git a/turbo-ext/src/ArgumentsHandler.cpp b/turbo-ext/src/ArgumentsHandler.cpp index f6dcb74d7fa..09845c30a28 100644 --- a/turbo-ext/src/ArgumentsHandler.cpp +++ b/turbo-ext/src/ArgumentsHandler.cpp @@ -860,17 +860,82 @@ class ArgumentsHandler zval *slot(uint32_t index) const { return OBJ_PROP_NUM(self, index); } - /* the `$getType = function (Expr $inner) use (&$getType, $nodeScopeResolver, - * $scope, $initializerContext): Type` of gatherArrayArgTypeSkeleton() — - * InitializerExprTypeResolver calls it synchronously, over this frame */ + /* The structural skeleton is priced in evaluation order over this frame. + * The array builder then reads the cached key/value types. */ struct SkeletonFrame { zend_object *self; zval *nodeScopeResolver; zval *scope; zval *initializerContext; + zv::Arr *types; + bool scopeIsValid = true; }; + static bool skeletonIs(zval *node, int id) + { + zend_class_entry *ce = pt_class(id); + return ce != NULL && Z_TYPE_P(node) == IS_OBJECT && instanceof_function(Z_OBJCE_P(node), ce); + } + + static bool skeletonChangesScope(zval *node, bool &out) + { + out = false; + bool isClosure, isArrow; + if (UNEXPECTED(!closureKind(node, isClosure, isArrow))) return false; + if (isClosure || isArrow) return true; + if (skeletonIs(node, PT_CLASS_CALL_LIKE)) { + bool firstClass; + if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(node), firstClass))) return false; + if (!firstClass) { out = true; return true; } + } + for (int id : {PT_CLASS_ASSIGN_EXPR, PT_CLASS_ASSIGN_REF_EXPR, PT_CLASS_ASSIGN_OP_EXPR, + PT_CLASS_PRE_INC, PT_CLASS_PRE_DEC, PT_CLASS_POST_INC, PT_CLASS_POST_DEC, + PT_CLASS_YIELD, PT_CLASS_YIELD_FROM, PT_CLASS_INCLUDE_EXPR, PT_CLASS_EVAL_EXPR}) { + if (skeletonIs(node, id)) { out = true; return true; } + } + if (UNEXPECTED(EG(exception))) return false; + static pt_method_site subNamesSite; + zv::Val names = callOn(subNamesSite, node, PT_LC("getsubnodenames"), "getSubNodeNames", 0, NULL); + if (UNEXPECTED(names.isUndef() || !requireArray(names.raw(), "getSubNodeNames()"))) return false; + for (zv::ArrayEntry entry : zv::ArrRef(names.raw())) { + zval *name = entry.value().deref().raw(); + if (UNEXPECTED(Z_TYPE_P(name) != IS_STRING)) { pt_throw_should_not_happen(); return false; } + zv::Val hold; + zval *child = readPropertySlow(node, Z_STRVAL_P(name), Z_STRLEN_P(name), hold); + if (UNEXPECTED(child == NULL)) return false; + auto inspect = [&](zval *value) -> bool { + ZVAL_DEREF(value); + if (!skeletonIs(value, PT_CLASS_NODE)) return !EG(exception); + return skeletonChangesScope(value, out); + }; + if (Z_TYPE_P(child) == IS_ARRAY) { + for (zv::ArrayEntry nested : zv::ArrRef(child)) { + if (UNEXPECTED(!inspect(nested.value().raw()))) return false; + if (out) return true; + } + } else { + if (UNEXPECTED(!inspect(child))) return false; + if (out) return true; + } + } + return true; + } + + static bool skeletonCallMatcher(zend_object *node, void *) + { + zend_class_entry *ce = pt_class(PT_CLASS_CALL_LIKE); + return ce != NULL && instanceof_function(node->ce, ce); + } + + static zv::Val skeletonCachedType(void *data, zval *inner) + { + zv::Arr *types = static_cast(data); + zval *type = readIndex(types->table(), Z_OBJ_HANDLE_P(inner)); + if (UNEXPECTED(type == NULL)) return zv::Val(); + return zv::Val::copyOf(zv::Ref(type)); + } + static zv::Val skeletonType(void *data, zval *inner) { SkeletonFrame *frame = static_cast(data); @@ -888,35 +953,108 @@ class ArgumentsHandler zend_class_entry *arrayExpr = pt_class(PT_CLASS_ARRAY_EXPR); if (UNEXPECTED(arrayExpr == NULL)) return zv::Val(); if (instanceof_function(Z_OBJCE_P(inner), arrayExpr)) { - pt_ietr_get_type getTypeCallback{&skeletonType, frame, &skeletonTypeCallable}; + zv::Val itemsHold; + zval *items = readPropertySlow(inner, PT_LC("items"), itemsHold); + if (UNEXPECTED(items == NULL || !requireArray(items, "items"))) return zv::Val(); + for (zv::ArrayEntry entry : zv::ArrRef(items)) { + zval *item = entry.value().deref().raw(); + zv::Val keyHold, valueHold, refHold; + zval *key = readPropertySlow(item, PT_LC("key"), keyHold); + if (UNEXPECTED(key == NULL)) return zv::Val(); + if (Z_TYPE_P(key) != IS_NULL) { + zv::Val type = skeletonType(frame, key); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + setKey(*frame->types, NULL, Z_OBJ_HANDLE_P(key), type.raw()); + } + zval *value = readPropertySlow(item, PT_LC("value"), valueHold); + if (UNEXPECTED(value == NULL)) return zv::Val(); + zv::Val type = skeletonType(frame, value); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + setKey(*frame->types, NULL, Z_OBJ_HANDLE_P(value), type.raw()); + zval *byRef = readPropertySlow(item, PT_LC("byRef"), refHold); + if (UNEXPECTED(byRef == NULL)) return zv::Val(); + if (zend_is_true(byRef)) frame->scopeIsValid = false; + } + pt_ietr_get_type getTypeCallback{&skeletonCachedType, frame->types, &skeletonTypeCallable}; return pt_initializer_expr_type_resolver_get_array_type(handler.slot(slots::initializerExprTypeResolver), inner, getTypeCallback); } - zv::Val stateType = pt_node_scope_resolver_find_scope_state_type(frame->nodeScopeResolver, inner, frame->scope); - if (UNEXPECTED(stateType.isUndef())) return zv::Val(); - if (!stateType.isNull()) return stateType; + bool changes; + if (UNEXPECTED(!skeletonChangesScope(inner, changes))) return zv::Val(); + if (changes) { + frame->scopeIsValid = false; + return pt_type_new_mixed_type(); + } + if (frame->scopeIsValid) { + zv::Val stateType = pt_node_scope_resolver_find_scope_state_type(frame->nodeScopeResolver, inner, frame->scope); + if (UNEXPECTED(stateType.isUndef())) return zv::Val(); + if (!stateType.isNull()) return stateType; + if (skeletonIs(inner, PT_CLASS_METHOD_CALL)) { + bool firstClass; + if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(inner), firstClass))) return zv::Val(); + zv::Val nameHold; + zval *name = readPropertySlow(inner, PT_LC("name"), nameHold); + if (UNEXPECTED(name == NULL)) return zv::Val(); + if (firstClass && skeletonIs(name, PT_CLASS_IDENTIFIER)) { + zv::Val varHold; + zval *var = readPropertySlow(inner, PT_LC("var"), varHold); + if (UNEXPECTED(var == NULL)) return zv::Val(); + zv::Val receiver = pt_node_scope_resolver_find_scope_state_type(frame->nodeScopeResolver, var, frame->scope); + if (UNEXPECTED(receiver.isUndef())) return zv::Val(); + if (!receiver.isNull()) { + zv::Val methodName = identifierToString(name); + if (UNEXPECTED(methodName.isUndef())) return zv::Val(); + zv::Val method = pt_mutating_scope_get_method_reflection(Z_OBJ_P(frame->scope), receiver.raw(), Z_STR_P(methodName.raw())); + if (UNEXPECTED(method.isUndef())) return zv::Val(); + if (!method.isNull()) { + zv::Val variants = pt_extended_method_reflection_call(method.raw(), PT_MR_GET_VARIANTS); + bool nativeTypesPromoted; + if (UNEXPECTED(variants.isUndef() || !pt_mutating_scope_native_types_promoted(Z_OBJ_P(frame->scope), nativeTypesPromoted))) return zv::Val(); + return pt_initializer_expr_type_resolver_create_first_class_callable(handler.slot(slots::initializerExprTypeResolver), method.raw(), variants.raw(), nativeTypesPromoted); + } + } + } + } + } + bool supported = false; + if (skeletonIs(inner, PT_CLASS_FUNC_CALL) || skeletonIs(inner, PT_CLASS_STATIC_CALL)) { + zv::Val nameHold; + zval *name = readPropertySlow(inner, PT_LC("name"), nameHold); + if (UNEXPECTED(name == NULL)) return zv::Val(); + if (skeletonIs(inner, PT_CLASS_FUNC_CALL)) { + supported = skeletonIs(name, PT_CLASS_NAME); + } else { + zv::Val classHold; + zval *className = readPropertySlow(inner, PT_LC("class"), classHold); + if (UNEXPECTED(className == NULL)) return zv::Val(); + supported = skeletonIs(className, PT_CLASS_NAME) && skeletonIs(name, PT_CLASS_IDENTIFIER); + } + } + if (!supported) { + pt_find_ctx ctx{}; + zend_object *found = pt_find_first_recursive(Z_OBJ_P(inner), skeletonCallMatcher, &ctx); + if (UNEXPECTED(ctx.failed || EG(exception))) return zv::Val(); + if (found != NULL) return pt_type_new_mixed_type(); + } return pt_initializer_expr_type_resolver_get_type(handler.slot(slots::initializerExprTypeResolver), inner, frame->initializerContext); } - /* the callback as a PHP callable that outlives the call: the closure - * over copies of what the frame points at */ + /* The builder's PHP callback captures the computed types by value. */ static zv::Val skeletonTypeCallable(void *data) { - SkeletonFrame *frame = static_cast(data); - zval self; - ZVAL_OBJ(&self, frame->self); - return pt_native_closure(&skeletonTypeBody, &self, frame->nodeScopeResolver, frame->scope, frame->initializerContext); + zv::Arr *types = static_cast(data); + zv::Val capture = zv::Val::copyOf(zv::Ref(types->raw())); + return pt_native_closure(&skeletonTypeBody, capture.raw()); } - /* the same closure called from PHP — captures: $this, $nodeScopeResolver, - * $scope, $initializerContext */ + /* The same cached-type callback called from PHP. */ static void skeletonTypeBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 1)) { zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 1 expected", PT_AH_CLOSURE("gatherArrayArgTypeSkeleton", "977"), argc); return; } - SkeletonFrame frame{Z_OBJ(captures[0]), &captures[1], &captures[2], &captures[3]}; - zv::Val type = skeletonType(&frame, &argv[0]); + zv::Arr types = zv::Arr::copyOfTable(Z_ARRVAL(captures[0])); + zv::Val type = skeletonCachedType(&types, &argv[0]); if (UNEXPECTED(type.isUndef())) return; type.intoReturnValue(return_value); } @@ -1044,16 +1182,17 @@ class ArgumentsHandler /* Mirrors gatherArrayArgTypeSkeleton() (private): a structural stand-in for * an array literal argument that holds closures, built without walking * anything - nested array literals recurse, a closure / arrow function - * contributes its declared signature, every other key/value is priced by - * the scope state it is tracked as, falling back to the constant-expression - * resolver. UNDEF = pending exception */ + * contributes its declared signature. Scope-known method callables retain + * their variants; later leaves cannot reuse state after an effect. Constants + * use the initializer resolver, walk-dependent expressions use mixed. + * UNDEF = pending exception */ zv::Val gatherArrayArgTypeSkeleton(zval *nodeScopeResolver, zval *expr, zval *scope) const { zv::Val initializerContext = pt_initializer_expr_context_from_scope(scope); if (UNEXPECTED(initializerContext.isUndef())) return zv::Val(); - SkeletonFrame frame{self, nodeScopeResolver, scope, initializerContext.raw()}; - pt_ietr_get_type getTypeCallback{&skeletonType, &frame, &skeletonTypeCallable}; - return pt_initializer_expr_type_resolver_get_array_type(slot(slots::initializerExprTypeResolver), expr, getTypeCallback); + zv::Arr types = zv::Arr::empty(); + SkeletonFrame frame{self, nodeScopeResolver, scope, initializerContext.raw(), &types}; + return skeletonType(&frame, expr); } /* Mirrors gatherClosureArgType() */