From b82594723c7f2e63635f1a490b5c3a61b6e7ca2f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 10:39:26 +0200 Subject: [PATCH 1/9] Collect the markers of an array literal or union once for all its items collectAbsorbed() collected the markers of the whole result for every input carrying any, so an array literal whose items are results of stored closures (a data provider calling `$item(...)` two thousand times) traversed the whole array type once per item, at every nesting level. collectAbsorbedInto() collects the result's markers once for all inputs of a literal or of a ternary/match/`??` union. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- src/Analyser/ExprHandler/ArrayHandler.php | 5 +- .../Generics/ClosureSignatureInference.php | 46 ++-- turbo-ext/src/ArrayHandler.cpp | 10 +- turbo-ext/src/ClosureSignatureInference.cpp | 54 +++-- .../src/generated/ClosureSignatureInference.h | 216 +++++++++--------- turbo-ext/src/support.h | 1 + 6 files changed, 185 insertions(+), 147 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index 9e3b11c2d3a..de7b6c6ea89 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -225,11 +225,8 @@ private function collectAbsorbedItems(Array_ $expr, array $itemResults, Mutating } $arrayType = $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $inner): Type => $itemResults[spl_object_id($inner)]->getType()); - foreach ($itemTypes as $itemType) { - $scope = $scope->addTemplateArgumentConstraints(ClosureSignatureInference::collectAbsorbed($itemType, $arrayType)); - } - return $scope; + return $scope->addTemplateArgumentConstraints(ClosureSignatureInference::collectAbsorbedInto($itemTypes, $arrayType)); } private function getExpectedArrayType(?Type $type): ?Type diff --git a/src/Analyser/Generics/ClosureSignatureInference.php b/src/Analyser/Generics/ClosureSignatureInference.php index d715734b5e2..c2062519c1c 100644 --- a/src/Analyser/Generics/ClosureSignatureInference.php +++ b/src/Analyser/Generics/ClosureSignatureInference.php @@ -289,21 +289,38 @@ public static function collectInvokedCallee(Type $calleeType): TemplateArgumentC * they escape. */ public static function collectAbsorbed(Type $input, Type $result): TemplateArgumentConstraints + { + return self::collectAbsorbedInto([$input], $result); + } + + /** + * The values of $inputs merged into $result - the items of an array + * literal, the members of a union. See collectAbsorbed(). The markers + * $result kept are collected once for all of them: a large literal of + * closure-carrying items would otherwise traverse the whole array type + * once per item. + * + * @param list $inputs + */ + public static function collectAbsorbedInto(array $inputs, Type $result): TemplateArgumentConstraints { $constraints = TemplateArgumentConstraints::createEmpty(); - if ($input === $result) { - return $constraints; - } - $markers = self::collectMarkers($input); - if ($markers === []) { - return $constraints; - } - $kept = self::collectMarkers($result); - foreach ($markers as $key => $marker) { - if (isset($kept[$key])) { + $kept = null; + foreach ($inputs as $input) { + if ($input === $result) { continue; } - $constraints = $constraints->withUnconstrainingSend($marker); + $markers = self::collectMarkers($input); + if ($markers === []) { + continue; + } + $kept ??= self::collectMarkers($result); + foreach ($markers as $key => $marker) { + if (isset($kept[$key])) { + continue; + } + $constraints = $constraints->withUnconstrainingSend($marker); + } } return $constraints; @@ -330,12 +347,7 @@ public static function collectAbsorbedInUnion(array $types): TemplateArgumentCon return $constraints; } - $union = TypeCombinator::union(...$types); - foreach ($types as $type) { - $constraints = $constraints->merge(self::collectAbsorbed($type, $union)); - } - - return $constraints; + return self::collectAbsorbedInto($types, TypeCombinator::union(...$types)); } /** Whether $type carries a closure written where nothing types it. */ diff --git a/turbo-ext/src/ArrayHandler.cpp b/turbo-ext/src/ArrayHandler.cpp index 4e999d6d1cc..ef35055b328 100644 --- a/turbo-ext/src/ArrayHandler.cpp +++ b/turbo-ext/src/ArrayHandler.cpp @@ -412,13 +412,9 @@ class ArrayHandler if (UNEXPECTED(arrayType.isUndef())) return zv::Val(); } zv::Val itemTypesHold(std::move(itemTypes)); - for (zv::ArrayEntry entry : zv::ArrRef(itemTypesHold.raw())) { - zv::Val absorbed = pt_closure_signature_inference_collect_absorbed(entry.value().deref().raw(), arrayType.raw()); - if (UNEXPECTED(absorbed.isUndef())) return zv::Val(); - scope = pt_mutating_scope_add_template_argument_constraints(Z_OBJ_P(scope.raw()), absorbed.raw()); - if (UNEXPECTED(scope.isUndef())) return zv::Val(); - } - return scope; + zv::Val absorbed = pt_closure_signature_inference_collect_absorbed_into(itemTypesHold.raw(), arrayType.raw()); + if (UNEXPECTED(absorbed.isUndef())) return zv::Val(); + return pt_mutating_scope_add_template_argument_constraints(Z_OBJ_P(scope.raw()), absorbed.raw()); } /* the handler entry (Engine.h) */ diff --git a/turbo-ext/src/ClosureSignatureInference.cpp b/turbo-ext/src/ClosureSignatureInference.cpp index 4dbe668636e..ac4334ac077 100644 --- a/turbo-ext/src/ClosureSignatureInference.cpp +++ b/turbo-ext/src/ClosureSignatureInference.cpp @@ -1167,16 +1167,34 @@ class ClosureSignatureInference /* Mirrors collectAbsorbed() */ static zv::Val collectAbsorbed(zval *input, zval *result) + { + zv::Arr inputs = zv::Arr::create(1); + inputs.push(zv::Val::copyOf(zv::Ref(input))); + zv::Val inputsHold(std::move(inputs)); + return collectAbsorbedInto(inputsHold.raw(), result); + } + + /* Mirrors collectAbsorbedInto(): the markers $result kept are collected + * once, on the first input carrying any */ + static zv::Val collectAbsorbedInto(zval *inputs, zval *result) { zv::Val constraints = pt_template_argument_constraints_create_empty(); if (UNEXPECTED(constraints.isUndef())) return zv::Val(); - if (Z_TYPE_P(input) == IS_OBJECT && Z_TYPE_P(result) == IS_OBJECT && Z_OBJ_P(input) == Z_OBJ_P(result)) return constraints; - zv::Val markers = collectMarkers(input); - if (UNEXPECTED(markers.isUndef())) return zv::Val(); - if (zend_hash_num_elements(Z_ARRVAL_P(markers.raw())) == 0) return constraints; - zv::Val kept = collectMarkers(result); - if (UNEXPECTED(kept.isUndef())) return zv::Val(); - return unconstrainMarkers(std::move(constraints), markers.raw(), kept.raw()); + zv::Val kept; + for (auto entry : zv::ArrRef(inputs)) { + zval *input = entry.value().deref().raw(); + if (Z_TYPE_P(input) == IS_OBJECT && Z_TYPE_P(result) == IS_OBJECT && Z_OBJ_P(input) == Z_OBJ_P(result)) continue; + zv::Val markers = collectMarkers(input); + if (UNEXPECTED(markers.isUndef())) return zv::Val(); + if (zend_hash_num_elements(Z_ARRVAL_P(markers.raw())) == 0) continue; + if (kept.isUndef()) { + kept = collectMarkers(result); + if (UNEXPECTED(kept.isUndef())) return zv::Val(); + } + constraints = unconstrainMarkers(std::move(constraints), markers.raw(), kept.raw()); + if (UNEXPECTED(constraints.isUndef())) return zv::Val(); + } + return constraints; } /* Mirrors hasMarkers() */ @@ -1212,13 +1230,7 @@ class ClosureSignatureInference zv::Val unionType = pt_type_combinator_union(count, argv); efree(argv); if (UNEXPECTED(unionType.isUndef())) return zv::Val(); - for (auto entry : zv::ArrRef(types)) { - zv::Val absorbed = collectAbsorbed(entry.value().deref().raw(), unionType.raw()); - if (UNEXPECTED(absorbed.isUndef())) return zv::Val(); - constraints = pt_template_argument_constraints_merge(constraints.raw(), absorbed.raw()); - if (UNEXPECTED(constraints.isUndef())) return zv::Val(); - } - return constraints; + return collectAbsorbedInto(types, unionType.raw()); } /* Mirrors collectInvocation() */ @@ -1847,6 +1859,11 @@ zv::Val pt_closure_signature_inference_collect_absorbed(zval *input, zval *resul return ClosureSignatureInference::collectAbsorbed(input, result); } +zv::Val pt_closure_signature_inference_collect_absorbed_into(zval *inputs, zval *result) +{ + return ClosureSignatureInference::collectAbsorbedInto(inputs, result); +} + zv::Val pt_closure_signature_inference_collect_absorbed_in_union(zval *types) { return ClosureSignatureInference::collectAbsorbedInUnion(types); @@ -2007,6 +2024,15 @@ PT_MINIT_REGISTRATION(pt_register_closure_signature_inference) PT_RETURN_VAL(ClosureSignatureInference::collectAbsorbed(input, result)); }); + cls.method(sigs::collectAbsorbedInto, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *inputs, *result; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(inputs) + Z_PARAM_OBJECT(result) + ZEND_PARSE_PARAMETERS_END(); + PT_RETURN_VAL(ClosureSignatureInference::collectAbsorbedInto(inputs, result)); + }); + cls.method(sigs::collectAbsorbedInUnion, [](INTERNAL_FUNCTION_PARAMETERS) { zval *types; ZEND_PARSE_PARAMETERS_START(1, 1) diff --git a/turbo-ext/src/generated/ClosureSignatureInference.h b/turbo-ext/src/generated/ClosureSignatureInference.h index 9d988e3a13f..b3eefcb2851 100644 --- a/turbo-ext/src/generated/ClosureSignatureInference.h +++ b/turbo-ext/src/generated/ClosureSignatureInference.h @@ -57,54 +57,56 @@ inline constexpr char strings[] = "input\0" /* 533 */ "result\0" /* 539 */ "collectAbsorbed\0" /* 546 */ - "types\0" /* 562 */ - "collectAbsorbedInUnion\0" /* 568 */ - "hasMarkers\0" /* 591 */ - "collectMarkers\0" /* 602 */ - "call\0" /* 617 */ - "PhpParser\\Node\\Expr\0" /* 622 */ - "closureType\0" /* 642 */ - "PHPStan\\Type\\ClosureType\0" /* 654 */ - "observing\0" /* 679 */ - "collectInvocation\0" /* 689 */ - "collectByRefEntryTypes\0" /* 707 */ - "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 730 */ - "getArrowFunctionOuterVariables\0" /* 764 */ - "byRefUseNames\0" /* 795 */ - "stmts\0" /* 809 */ - "names\0" /* 815 */ - "bodyYieldsOrInvokes\0" /* 821 */ - "body\0" /* 841 */ - "PhpParser\\Node\0" /* 846 */ - "getReboundVariableNames\0" /* 861 */ - "node\0" /* 885 */ - "collectVariableName\0" /* 890 */ - "target\0" /* 910 */ - "collectByRefItemNames\0" /* 917 */ - "parameterName\0" /* 939 */ - "parameterTemplateName\0" /* 953 */ - "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame\0" /* 975 */ - "getFrame\0" /* 1023 */ - "isObserving\0" /* 1032 */ - "declaredParameters\0" /* 1044 */ - "getSignatureParameters\0" /* 1063 */ - "getBodyParameters\0" /* 1086 */ - "returnType\0" /* 1104 */ - "getSignatureReturnType\0" /* 1115 */ - "getExpectedReturnType\0" /* 1138 */ - "collectSites\0" /* 1160 */ - "parameter\0" /* 1173 */ - "PHPStan\\Reflection\\Native\\NativeParameterReflection\0" /* 1183 */ - "createParameterMarker\0" /* 1235 */ - "returnsContextTypedExpression\0" /* 1257 */ - "functionLike\0" /* 1287 */ - "isClosedBody\0" /* 1300 */ - "scanClosedBody\0" /* 1313 */ - "infersInvocationReturnType\0" /* 1328 */ - "findAssignedClosures\0" /* 1355 */ - "getAssignedClosures\0" /* 1376 */ - "collectTargetNames\0" /* 1396 */ - "isContextTyped"; /* 1415 */ + "inputs\0" /* 562 */ + "collectAbsorbedInto\0" /* 569 */ + "types\0" /* 589 */ + "collectAbsorbedInUnion\0" /* 595 */ + "hasMarkers\0" /* 618 */ + "collectMarkers\0" /* 629 */ + "call\0" /* 644 */ + "PhpParser\\Node\\Expr\0" /* 649 */ + "closureType\0" /* 669 */ + "PHPStan\\Type\\ClosureType\0" /* 681 */ + "observing\0" /* 706 */ + "collectInvocation\0" /* 716 */ + "collectByRefEntryTypes\0" /* 734 */ + "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 757 */ + "getArrowFunctionOuterVariables\0" /* 791 */ + "byRefUseNames\0" /* 822 */ + "stmts\0" /* 836 */ + "names\0" /* 842 */ + "bodyYieldsOrInvokes\0" /* 848 */ + "body\0" /* 868 */ + "PhpParser\\Node\0" /* 873 */ + "getReboundVariableNames\0" /* 888 */ + "node\0" /* 912 */ + "collectVariableName\0" /* 917 */ + "target\0" /* 937 */ + "collectByRefItemNames\0" /* 944 */ + "parameterName\0" /* 966 */ + "parameterTemplateName\0" /* 980 */ + "PHPStan\\Analyser\\Generics\\TemplateArgumentFrame\0" /* 1002 */ + "getFrame\0" /* 1050 */ + "isObserving\0" /* 1059 */ + "declaredParameters\0" /* 1071 */ + "getSignatureParameters\0" /* 1090 */ + "getBodyParameters\0" /* 1113 */ + "returnType\0" /* 1131 */ + "getSignatureReturnType\0" /* 1142 */ + "getExpectedReturnType\0" /* 1165 */ + "collectSites\0" /* 1187 */ + "parameter\0" /* 1200 */ + "PHPStan\\Reflection\\Native\\NativeParameterReflection\0" /* 1210 */ + "createParameterMarker\0" /* 1262 */ + "returnsContextTypedExpression\0" /* 1284 */ + "functionLike\0" /* 1314 */ + "isClosedBody\0" /* 1327 */ + "scanClosedBody\0" /* 1340 */ + "infersInvocationReturnType\0" /* 1355 */ + "findAssignedClosures\0" /* 1382 */ + "getAssignedClosures\0" /* 1403 */ + "collectTargetNames\0" /* 1423 */ + "isContextTyped"; /* 1442 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_BOOL), /* __construct $enabled */ reg::packed(20, 0, 27), /* isClosureSignatureMarker $marker */ @@ -136,82 +138,85 @@ inline constexpr reg::PackedArg args[] = { reg::packed(533, 0, 307), /* collectAbsorbed $input */ reg::packed(539, 0, 307), /* collectAbsorbed $result */ reg::packed(79, 0, 410), /* collectAbsorbed return */ - reg::packed(562, MAY_BE_ARRAY), /* collectAbsorbedInUnion $types */ + reg::packed(562, MAY_BE_ARRAY), /* collectAbsorbedInto $inputs */ + reg::packed(539, 0, 307), /* collectAbsorbedInto $result */ + reg::packed(79, 0, 410), /* collectAbsorbedInto return */ + reg::packed(589, MAY_BE_ARRAY), /* collectAbsorbedInUnion $types */ reg::packed(79, 0, 410), /* collectAbsorbedInUnion return */ reg::packed(405, 0, 307), /* hasMarkers $type */ reg::packed(79, MAY_BE_BOOL), /* hasMarkers return */ reg::packed(405, 0, 307), /* collectMarkers $type */ reg::packed(79, MAY_BE_ARRAY), /* collectMarkers return */ reg::packed(134, 0, 140), /* collectInvocation $scope */ - reg::packed(617, 0, 622), /* collectInvocation $call */ - reg::packed(642, 0, 654), /* collectInvocation $closureType */ - reg::packed(679, MAY_BE_BOOL), /* collectInvocation $observing */ + reg::packed(644, 0, 649), /* collectInvocation $call */ + reg::packed(669, 0, 681), /* collectInvocation $closureType */ + reg::packed(706, MAY_BE_BOOL), /* collectInvocation $observing */ reg::packed(79, 0, 410), /* collectInvocation return */ reg::packed(134, 0, 140), /* collectByRefEntryTypes $scope */ reg::packed(79, MAY_BE_ARRAY), /* collectByRefEntryTypes return */ - reg::packed(171, 0, 730), /* getArrowFunctionOuterVariables $expr */ + reg::packed(171, 0, 757), /* getArrowFunctionOuterVariables $expr */ reg::packed(79, MAY_BE_ARRAY), /* getArrowFunctionOuterVariables return */ reg::packed(171, 0, 257), /* byRefUseNames $expr */ reg::packed(79, MAY_BE_ARRAY), /* byRefUseNames return */ - reg::packed(809, MAY_BE_ARRAY), /* bodyYieldsOrInvokes $stmts */ - reg::packed(815, MAY_BE_ARRAY), /* bodyYieldsOrInvokes $names */ + reg::packed(836, MAY_BE_ARRAY), /* bodyYieldsOrInvokes $stmts */ + reg::packed(842, MAY_BE_ARRAY), /* bodyYieldsOrInvokes $names */ reg::packed(79, MAY_BE_BOOL), /* bodyYieldsOrInvokes return */ - reg::packed(841, 0, 846), /* getReboundVariableNames $body */ - reg::packed(809, MAY_BE_ARRAY), /* getReboundVariableNames $stmts */ + reg::packed(868, 0, 873), /* getReboundVariableNames $body */ + reg::packed(836, MAY_BE_ARRAY), /* getReboundVariableNames $stmts */ reg::packed(79, MAY_BE_ARRAY), /* getReboundVariableNames return */ - reg::packed(885, 0, 846), /* collectVariableName $node */ - reg::packed(815, MAY_BE_ARRAY, reg::NoString, true, false), /* collectVariableName $names */ + reg::packed(912, 0, 873), /* collectVariableName $node */ + reg::packed(842, MAY_BE_ARRAY, reg::NoString, true, false), /* collectVariableName $names */ reg::packed(79, MAY_BE_VOID), /* collectVariableName return */ - reg::packed(910, 0, 622), /* collectByRefItemNames $target */ - reg::packed(815, MAY_BE_ARRAY, reg::NoString, true, false), /* collectByRefItemNames $names */ + reg::packed(937, 0, 649), /* collectByRefItemNames $target */ + reg::packed(842, MAY_BE_ARRAY, reg::NoString, true, false), /* collectByRefItemNames $names */ reg::packed(79, MAY_BE_VOID), /* collectByRefItemNames return */ - reg::packed(939, MAY_BE_STRING), /* parameterTemplateName $parameterName */ + reg::packed(966, MAY_BE_STRING), /* parameterTemplateName $parameterName */ reg::packed(79, MAY_BE_STRING), /* parameterTemplateName return */ reg::packed(134, 0, 140), /* getFrame $scope */ - reg::packed(79, MAY_BE_NULL, 975), /* getFrame return */ + reg::packed(79, MAY_BE_NULL, 1002), /* getFrame return */ reg::packed(134, 0, 140), /* isObserving $scope */ reg::packed(79, MAY_BE_BOOL), /* isObserving return */ reg::packed(134, 0, 140), /* getSignatureParameters $scope */ reg::packed(171, 0, 176), /* getSignatureParameters $expr */ - reg::packed(1044, MAY_BE_ARRAY), /* getSignatureParameters $declaredParameters */ + reg::packed(1071, MAY_BE_ARRAY), /* getSignatureParameters $declaredParameters */ reg::packed(79, MAY_BE_ARRAY), /* getSignatureParameters return */ reg::packed(134, 0, 140), /* getBodyParameters $scope */ reg::packed(171, 0, 176), /* getBodyParameters $expr */ reg::packed(79, MAY_BE_NULL | MAY_BE_ARRAY), /* getBodyParameters return */ reg::packed(134, 0, 140), /* getSignatureReturnType $scope */ reg::packed(171, 0, 176), /* getSignatureReturnType $expr */ - reg::packed(1104, 0, 307), /* getSignatureReturnType $returnType */ + reg::packed(1131, 0, 307), /* getSignatureReturnType $returnType */ reg::packed(79, 0, 307), /* getSignatureReturnType return */ reg::packed(134, 0, 140), /* getExpectedReturnType $scope */ reg::packed(171, 0, 176), /* getExpectedReturnType $expr */ reg::packed(79, MAY_BE_NULL, 307), /* getExpectedReturnType return */ reg::packed(134, 0, 140), /* collectSites $scope */ - reg::packed(642, 0, 307), /* collectSites $closureType */ + reg::packed(669, 0, 307), /* collectSites $closureType */ reg::packed(79, 0, 410), /* collectSites return */ reg::packed(171, 0, 176), /* createParameterMarker $expr */ - reg::packed(1173, 0, 1183), /* createParameterMarker $parameter */ + reg::packed(1200, 0, 1210), /* createParameterMarker $parameter */ reg::packed(79, 0, 27), /* createParameterMarker return */ reg::packed(171, 0, 176), /* returnsContextTypedExpression $expr */ reg::packed(79, MAY_BE_BOOL), /* returnsContextTypedExpression return */ - reg::packed(1287, 0, 846), /* isClosedBody $functionLike */ - reg::packed(809, MAY_BE_ARRAY), /* isClosedBody $stmts */ + reg::packed(1314, 0, 873), /* isClosedBody $functionLike */ + reg::packed(836, MAY_BE_ARRAY), /* isClosedBody $stmts */ reg::packed(79, MAY_BE_BOOL), /* isClosedBody return */ - reg::packed(1287, 0, 846), /* scanClosedBody $functionLike */ - reg::packed(809, MAY_BE_ARRAY), /* scanClosedBody $stmts */ + reg::packed(1314, 0, 873), /* scanClosedBody $functionLike */ + reg::packed(836, MAY_BE_ARRAY), /* scanClosedBody $stmts */ reg::packed(79, MAY_BE_BOOL), /* scanClosedBody return */ reg::packed(134, 0, 140), /* infersInvocationReturnType $scope */ - reg::packed(642, 0, 654), /* infersInvocationReturnType $closureType */ + reg::packed(669, 0, 681), /* infersInvocationReturnType $closureType */ reg::packed(79, MAY_BE_BOOL), /* infersInvocationReturnType return */ reg::packed(134, 0, 140), /* findAssignedClosures $scope */ reg::packed(302, MAY_BE_STRING), /* findAssignedClosures $name */ reg::packed(79, MAY_BE_ARRAY), /* findAssignedClosures return */ - reg::packed(841, 0, 846), /* getAssignedClosures $body */ - reg::packed(809, MAY_BE_ARRAY), /* getAssignedClosures $stmts */ + reg::packed(868, 0, 873), /* getAssignedClosures $body */ + reg::packed(836, MAY_BE_ARRAY), /* getAssignedClosures $stmts */ reg::packed(79, MAY_BE_ARRAY), /* getAssignedClosures return */ - reg::packed(910, 0, 622), /* collectTargetNames $target */ - reg::packed(815, MAY_BE_ARRAY, reg::NoString, true, false), /* collectTargetNames $names */ + reg::packed(937, 0, 649), /* collectTargetNames $target */ + reg::packed(842, MAY_BE_ARRAY, reg::NoString, true, false), /* collectTargetNames $names */ reg::packed(79, MAY_BE_VOID), /* collectTargetNames return */ - reg::packed(171, 0, 622), /* isContextTyped $expr */ + reg::packed(171, 0, 649), /* isContextTyped $expr */ reg::packed(79, MAY_BE_BOOL), /* isContextTyped return */ }; using Sig = reg::Sig; @@ -231,34 +236,35 @@ inline constexpr sigtab::Sig collectCaptureEscapes = { { 464 /* collectCaptureEs inline constexpr sigtab::Sig collectEscapes = { { 486 /* collectEscapes */, 1, 23, 1, 24, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig collectInvokedCallee = { { 512 /* collectInvokedCallee */, 1, 25, 1, 26, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig collectAbsorbed = { { 546 /* collectAbsorbed */, 2, 27, 2, 29, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectAbsorbedInUnion = { { 568 /* collectAbsorbedInUnion */, 1, 30, 1, 31, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig hasMarkers = { { 591 /* hasMarkers */, 1, 32, 1, 33, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectMarkers = { { 602 /* collectMarkers */, 1, 34, 1, 35, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectInvocation = { { 689 /* collectInvocation */, 4, 36, 4, 40, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectByRefEntryTypes = { { 707 /* collectByRefEntryTypes */, 1, 41, 1, 42, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getArrowFunctionOuterVariables = { { 764 /* getArrowFunctionOuterVariables */, 1, 43, 1, 44, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig byRefUseNames = { { 795 /* byRefUseNames */, 1, 45, 1, 46, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig bodyYieldsOrInvokes = { { 821 /* bodyYieldsOrInvokes */, 2, 47, 2, 49, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getReboundVariableNames = { { 861 /* getReboundVariableNames */, 2, 50, 2, 52, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectVariableName = { { 890 /* collectVariableName */, 2, 53, 2, 55, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectByRefItemNames = { { 917 /* collectByRefItemNames */, 2, 56, 2, 58, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig parameterTemplateName = { { 953 /* parameterTemplateName */, 1, 59, 1, 60, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getFrame = { { 1023 /* getFrame */, 1, 61, 1, 62, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isObserving = { { 1032 /* isObserving */, 1, 63, 1, 64, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getSignatureParameters = { { 1063 /* getSignatureParameters */, 3, 65, 3, 68, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getBodyParameters = { { 1086 /* getBodyParameters */, 2, 69, 2, 71, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getSignatureReturnType = { { 1115 /* getSignatureReturnType */, 3, 72, 3, 75, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getExpectedReturnType = { { 1138 /* getExpectedReturnType */, 2, 76, 2, 78, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig collectSites = { { 1160 /* collectSites */, 2, 79, 2, 81, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig createParameterMarker = { { 1235 /* createParameterMarker */, 2, 82, 2, 84, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig returnsContextTypedExpression = { { 1257 /* returnsContextTypedExpression */, 1, 85, 1, 86, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig isClosedBody = { { 1300 /* isClosedBody */, 2, 87, 2, 89, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig scanClosedBody = { { 1313 /* scanClosedBody */, 2, 90, 2, 92, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig infersInvocationReturnType = { { 1328 /* infersInvocationReturnType */, 2, 93, 2, 95, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig findAssignedClosures = { { 1355 /* findAssignedClosures */, 2, 96, 2, 98, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getAssignedClosures = { { 1376 /* getAssignedClosures */, 2, 99, 2, 101, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectTargetNames = { { 1396 /* collectTargetNames */, 2, 102, 2, 104, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig isContextTyped = { { 1415 /* isContextTyped */, 1, 105, 1, 106, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectAbsorbedInto = { { 569 /* collectAbsorbedInto */, 2, 30, 2, 32, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectAbsorbedInUnion = { { 595 /* collectAbsorbedInUnion */, 1, 33, 1, 34, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig hasMarkers = { { 618 /* hasMarkers */, 1, 35, 1, 36, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectMarkers = { { 629 /* collectMarkers */, 1, 37, 1, 38, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectInvocation = { { 716 /* collectInvocation */, 4, 39, 4, 43, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectByRefEntryTypes = { { 734 /* collectByRefEntryTypes */, 1, 44, 1, 45, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getArrowFunctionOuterVariables = { { 791 /* getArrowFunctionOuterVariables */, 1, 46, 1, 47, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig byRefUseNames = { { 822 /* byRefUseNames */, 1, 48, 1, 49, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig bodyYieldsOrInvokes = { { 848 /* bodyYieldsOrInvokes */, 2, 50, 2, 52, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getReboundVariableNames = { { 888 /* getReboundVariableNames */, 2, 53, 2, 55, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectVariableName = { { 917 /* collectVariableName */, 2, 56, 2, 58, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectByRefItemNames = { { 944 /* collectByRefItemNames */, 2, 59, 2, 61, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig parameterTemplateName = { { 980 /* parameterTemplateName */, 1, 62, 1, 63, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getFrame = { { 1050 /* getFrame */, 1, 64, 1, 65, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isObserving = { { 1059 /* isObserving */, 1, 66, 1, 67, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSignatureParameters = { { 1090 /* getSignatureParameters */, 3, 68, 3, 71, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getBodyParameters = { { 1113 /* getBodyParameters */, 2, 72, 2, 74, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSignatureReturnType = { { 1142 /* getSignatureReturnType */, 3, 75, 3, 78, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getExpectedReturnType = { { 1165 /* getExpectedReturnType */, 2, 79, 2, 81, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig collectSites = { { 1187 /* collectSites */, 2, 82, 2, 84, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig createParameterMarker = { { 1262 /* createParameterMarker */, 2, 85, 2, 87, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig returnsContextTypedExpression = { { 1284 /* returnsContextTypedExpression */, 1, 88, 1, 89, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isClosedBody = { { 1327 /* isClosedBody */, 2, 90, 2, 92, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig scanClosedBody = { { 1340 /* scanClosedBody */, 2, 93, 2, 95, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig infersInvocationReturnType = { { 1355 /* infersInvocationReturnType */, 2, 96, 2, 98, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig findAssignedClosures = { { 1382 /* findAssignedClosures */, 2, 99, 2, 101, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAssignedClosures = { { 1403 /* getAssignedClosures */, 2, 102, 2, 104, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectTargetNames = { { 1423 /* collectTargetNames */, 2, 105, 2, 107, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isContextTyped = { { 1442 /* isContextTyped */, 1, 108, 1, 109, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::ClosureSignatureInference diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 832fa6c67b1..96cf47dd3b5 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -3128,6 +3128,7 @@ zv::Val pt_closure_signature_inference_get_arrow_function_outer_variables(zval * zv::Val pt_closure_signature_inference_collect_escapes(zval *type); zv::Val pt_closure_signature_inference_collect_invoked_callee(zval *calleeType); zv::Val pt_closure_signature_inference_collect_absorbed(zval *input, zval *result); +zv::Val pt_closure_signature_inference_collect_absorbed_into(zval *inputs, zval *result); zv::Val pt_closure_signature_inference_collect_absorbed_in_union(zval *types); [[nodiscard]] bool pt_closure_signature_inference_has_markers(zval *type, bool &out); /* the handlers' `$scope->addTemplateArgumentConstraints( From 4bbf5c7976991bf1314e40c11875ca22b63f7b7d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 10:39:26 +0200 Subject: [PATCH 2/9] Do not build the value type of an array sent to plain mixed Sending an array to `array` (a function's return type, a property) recursed into getIterableValueType() of the sent array - for a large constant array the union of all its values - only to observe nothing in observeSend() and to let everything escape in observeClosureSend(). Outside a call argument observeSend() skips plain mixed key and value targets, and observeClosureSend() lets the closures in the array itself escape. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- .../Generics/TemplateArgumentObserver.php | 30 +++++++++++++++++-- turbo-ext/src/TemplateArgumentObserver.cpp | 29 +++++++++++++++--- .../src/generated/TemplateArgumentObserver.h | 25 +++++++++------- 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/src/Analyser/Generics/TemplateArgumentObserver.php b/src/Analyser/Generics/TemplateArgumentObserver.php index c96bc05d3d2..c3eaf43a5d4 100644 --- a/src/Analyser/Generics/TemplateArgumentObserver.php +++ b/src/Analyser/Generics/TemplateArgumentObserver.php @@ -192,8 +192,14 @@ private function observeClosureSend(TemplateArgumentConstraints $constraints, Ty && $declared->isIterable()->yes() ) { $constraints = $this->observeClosureSend($constraints, $declared->getIterableKeyType(), $actual->getIterableKeyType()); + $declaredValueType = $declared->getIterableValueType(); + if ($declaredValueType instanceof MixedType && !$declaredValueType instanceof TemplateType) { + // every value sent to plain mixed escapes - collected from the + // array itself, not from the union of a large constant array's values + return $this->escapeClosures($constraints, $actual); + } - return $this->observeClosureSend($constraints, $declared->getIterableValueType(), $actual->getIterableValueType()); + return $this->observeClosureSend($constraints, $declaredValueType, $actual->getIterableValueType()); } return $this->escapeClosures($constraints, $actual); @@ -501,12 +507,30 @@ private function observeSend(TemplateArgumentConstraints $constraints, Type $dec return $constraints; } - $constraints = $this->observeSend($constraints, $declared->getIterableKeyType(), $actual->getIterableKeyType(), $isCallArgument); - $constraints = $this->observeSend($constraints, $declared->getIterableValueType(), $actual->getIterableValueType(), $isCallArgument); + // the key and value types of a large constant array are the unions of + // all its keys and values - not built for a target they cannot constrain + $declaredKeyType = $declared->getIterableKeyType(); + if (!self::isInertSendTarget($declaredKeyType, $isCallArgument)) { + $constraints = $this->observeSend($constraints, $declaredKeyType, $actual->getIterableKeyType(), $isCallArgument); + } + $declaredValueType = $declared->getIterableValueType(); + if (!self::isInertSendTarget($declaredValueType, $isCallArgument)) { + $constraints = $this->observeSend($constraints, $declaredValueType, $actual->getIterableValueType(), $isCallArgument); + } return $constraints; } + /** + * observeSend() observes nothing sent to plain mixed outside a call + * argument: a union recurses into its members, a bare marker never + * constrains, mixed names no class and is not certainly iterable. + */ + private static function isInertSendTarget(Type $declared, bool $isCallArgument): bool + { + return !$isCallArgument && $declared instanceof MixedType && !$declared instanceof TemplateType; + } + /** * An argument was passed to a parameter: the argument's markers are sent to * the parameter type, and a parameter type carrying the receiver's markers diff --git a/turbo-ext/src/TemplateArgumentObserver.cpp b/turbo-ext/src/TemplateArgumentObserver.cpp index a8d1026c7f5..198de1c0a11 100644 --- a/turbo-ext/src/TemplateArgumentObserver.cpp +++ b/turbo-ext/src/TemplateArgumentObserver.cpp @@ -343,6 +343,11 @@ class TemplateArgumentObserver if (UNEXPECTED(constraints.isUndef())) return zv::Val(); zv::Val declaredValue = pt_type_op(Z_OBJ_P(declared), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); if (UNEXPECTED(declaredValue.isUndef())) return zv::Val(); + bool plainMixed; + if (UNEXPECTED(!isPlainMixed(declaredValue.raw(), plainMixed))) return zv::Val(); + if (plainMixed) { + return escapeClosures(std::move(constraints), actual); + } zv::Val actualValue = pt_type_op(Z_OBJ_P(actual), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); if (UNEXPECTED(actualValue.isUndef())) return zv::Val(); return observeClosureSend(std::move(constraints), declaredValue.raw(), actualValue.raw()); @@ -842,17 +847,33 @@ class TemplateArgumentObserver zv::Val declaredKey = pt_type_op(Z_OBJ_P(declared), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); if (UNEXPECTED(declaredKey.isUndef())) return zv::Val(); - zv::Val actualKey = pt_type_op(Z_OBJ_P(actual), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); - if (UNEXPECTED(actualKey.isUndef())) return zv::Val(); - constraints = observeSend(std::move(constraints), declaredKey.raw(), actualKey.raw(), isCallArgument); - if (UNEXPECTED(constraints.isUndef())) return zv::Val(); + bool inert; + if (UNEXPECTED(!isInertSendTarget(declaredKey.raw(), isCallArgument, inert))) return zv::Val(); + if (!inert) { + zv::Val actualKey = pt_type_op(Z_OBJ_P(actual), PT_OP_GET_ITERABLE_KEY_TYPE, 0, NULL); + if (UNEXPECTED(actualKey.isUndef())) return zv::Val(); + constraints = observeSend(std::move(constraints), declaredKey.raw(), actualKey.raw(), isCallArgument); + if (UNEXPECTED(constraints.isUndef())) return zv::Val(); + } zv::Val declaredValue = pt_type_op(Z_OBJ_P(declared), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); if (UNEXPECTED(declaredValue.isUndef())) return zv::Val(); + if (UNEXPECTED(!isInertSendTarget(declaredValue.raw(), isCallArgument, inert))) return zv::Val(); + if (inert) return constraints; zv::Val actualValue = pt_type_op(Z_OBJ_P(actual), PT_OP_GET_ITERABLE_VALUE_TYPE, 0, NULL); if (UNEXPECTED(actualValue.isUndef())) return zv::Val(); return observeSend(std::move(constraints), declaredValue.raw(), actualValue.raw(), isCallArgument); } + /* Mirrors isInertSendTarget(); false = pending exception */ + [[nodiscard]] static bool isInertSendTarget(zval *declared, bool isCallArgument, bool &out) + { + if (isCallArgument) { + out = false; + return true; + } + return isPlainMixed(declared, out); + } + /* Mirrors observeArgument(). */ static zv::Val observeArgument(zv::Val constraints, zval *parameterType, zval *argumentType) { diff --git a/turbo-ext/src/generated/TemplateArgumentObserver.h b/turbo-ext/src/generated/TemplateArgumentObserver.h index fd44452cb68..03d1f36b851 100644 --- a/turbo-ext/src/generated/TemplateArgumentObserver.h +++ b/turbo-ext/src/generated/TemplateArgumentObserver.h @@ -63,11 +63,12 @@ inline constexpr char strings[] = "replaceInferableTemplates\0" /* 634 */ "isCallArgument\0" /* 660 */ "observeSend\0" /* 675 */ - "observeArgument\0" /* 687 */ - "observeLowerBound\0" /* 703 */ - "declaredArgument\0" /* 721 */ - "isUninformativeSendTarget\0" /* 738 */ - "hasOnlyInferableTemplates"; /* 764 */ + "isInertSendTarget\0" /* 687 */ + "observeArgument\0" /* 705 */ + "observeLowerBound\0" /* 721 */ + "declaredArgument\0" /* 739 */ + "isUninformativeSendTarget\0" /* 756 */ + "hasOnlyInferableTemplates"; /* 782 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 5), /* collectSites $type */ reg::packed(23, 0, 24), /* collectSites return */ @@ -123,6 +124,9 @@ inline constexpr reg::PackedArg args[] = { reg::packed(143, 0, 5), /* observeSend $actual */ reg::packed(660, MAY_BE_BOOL, reg::NoString, false, false, 113), /* observeSend $isCallArgument */ reg::packed(23, 0, 24), /* observeSend return */ + reg::packed(134, 0, 5), /* isInertSendTarget $declared */ + reg::packed(660, MAY_BE_BOOL), /* isInertSendTarget $isCallArgument */ + reg::packed(23, MAY_BE_BOOL), /* isInertSendTarget return */ reg::packed(386, 0, 24), /* observeArgument $constraints */ reg::packed(162, 0, 5), /* observeArgument $parameterType */ reg::packed(176, 0, 5), /* observeArgument $argumentType */ @@ -131,7 +135,7 @@ inline constexpr reg::PackedArg args[] = { reg::packed(162, 0, 5), /* observeLowerBound $parameterType */ reg::packed(176, 0, 5), /* observeLowerBound $argumentType */ reg::packed(23, 0, 24), /* observeLowerBound return */ - reg::packed(721, 0, 5), /* isUninformativeSendTarget $declaredArgument */ + reg::packed(739, 0, 5), /* isUninformativeSendTarget $declaredArgument */ reg::packed(23, MAY_BE_BOOL), /* isUninformativeSendTarget return */ reg::packed(0, 0, 5), /* hasOnlyInferableTemplates $type */ reg::packed(23, MAY_BE_BOOL), /* hasOnlyInferableTemplates return */ @@ -157,10 +161,11 @@ inline constexpr sigtab::Sig collectArgument = { { 514 /* collectArgument */, 2, inline constexpr sigtab::Sig collectCall = { { 612 /* collectCall */, 3, 39, 4, 43, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig replaceInferableTemplates = { { 634 /* replaceInferableTemplates */, 4, 44, 4, 48, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig observeSend = { { 675 /* observeSend */, 3, 49, 4, 53, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig observeArgument = { { 687 /* observeArgument */, 3, 54, 3, 57, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig observeLowerBound = { { 703 /* observeLowerBound */, 3, 58, 3, 61, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isUninformativeSendTarget = { { 738 /* isUninformativeSendTarget */, 1, 62, 1, 63, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig hasOnlyInferableTemplates = { { 764 /* hasOnlyInferableTemplates */, 1, 64, 1, 65, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isInertSendTarget = { { 687 /* isInertSendTarget */, 2, 54, 2, 56, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig observeArgument = { { 705 /* observeArgument */, 3, 57, 3, 60, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig observeLowerBound = { { 721 /* observeLowerBound */, 3, 61, 3, 64, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isUninformativeSendTarget = { { 756 /* isUninformativeSendTarget */, 1, 65, 1, 66, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig hasOnlyInferableTemplates = { { 782 /* hasOnlyInferableTemplates */, 1, 67, 1, 68, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::TemplateArgumentObserver From 766dcc4a6703d631f1cbffc23ada34558c66c074 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 11:23:53 +0200 Subject: [PATCH 3/9] Walk a by-ref closure's body once per state it is invoked from Every invocation of a closure whose by-ref uses are followed walked its body - until the by-ref variables converge while observing - even when an earlier invocation in the same pass entered it in the same state: a data provider invoking `$room(...)` 362 times walked the body 1297 times. The walks are kept in the ExpressionResultStorage of the walk they run in, keyed by the closure, the template argument frame, whether they ran until the fixpoint, whether native types are promoted, and the entered scope. An invocation entering an equal scope takes the stored exit scope and throw points and applies them to its own scope and call, as the walk would. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- src/Analyser/ClosureProcessor.php | 61 +++++++++- src/Analyser/ExpressionResultStorage.php | 38 ++++++ turbo-ext/src/ClosureProcessor.cpp | 115 ++++++++++++++---- turbo-ext/src/ExpressionResultStorage.cpp | 88 ++++++++++++++ turbo-ext/src/generated/ClosureProcessor.h | 97 ++++++++------- .../src/generated/ExpressionResultStorage.h | 16 ++- turbo-ext/src/support.h | 4 + turbo-ext/tests/signature-parity.php | 1 + turbo-ext/tests/smoke.php | 15 +++ 9 files changed, 360 insertions(+), 75 deletions(-) diff --git a/src/Analyser/ClosureProcessor.php b/src/Analyser/ClosureProcessor.php index 88d632b1e99..f1b2f89a9e2 100644 --- a/src/Analyser/ClosureProcessor.php +++ b/src/Analyser/ClosureProcessor.php @@ -434,6 +434,60 @@ public function processByRefInvocation( $enter = fn (MutatingScope $byRefSource): MutatingScope => $this->enterWithCapturedUses($scope, $expr, $callableParameters, $creationScope)->processClosureScope($byRefSource, null, $byRefUses); $entryScope = $enter($scope); + $frame = $scope->getCurrentTemplateArgumentFrame(); + $walk = null; + // the body entered in a state it was already walked from, in the same + // pass: a closure invoked many times mostly re-enters one of a few states + foreach ($storage->findByRefInvocationWalks($expr) as $storedWalk) { + [$walkFrame, $walkUntilFixpoint, $walkNativeTypesPromoted, $walkEntryScope] = $storedWalk; + if ( + $walkFrame !== $frame + || $walkUntilFixpoint !== $untilFixpoint + || $walkNativeTypesPromoted !== $scope->nativeTypesPromoted + || !$walkEntryScope->equals($entryScope) + ) { + continue; + } + $walk = $storedWalk; + break; + } + if ($walk === null) { + [$exitScope, $bodyThrowPoints] = $this->walkByRefInvocation($nodeScopeResolver, $expr, $scope, $storage, $entryScope, $enter, $byRefUses, $untilFixpoint); + $walk = [$frame, $untilFixpoint, $scope->nativeTypesPromoted, $entryScope, $exitScope, $bodyThrowPoints]; + $storage->storeByRefInvocationWalk($expr, $walk); + } + [, , , , $exitScope, $bodyThrowPoints] = $walk; + + $throwPoints = []; + foreach ($bodyThrowPoints as $throwPoint) { + $throwScope = $this->assignByRefUses($scope, $throwPoint->getScope(), $byRefUses); + $throwPoints[] = $throwPoint->isExplicit() + ? InternalThrowPoint::createExplicit($throwScope, $throwPoint->getType(), $call, $throwPoint->canContainAnyThrowable()) + : InternalThrowPoint::createImplicit($throwScope, $call); + } + + return [$this->assignByRefUses($scope, $exitScope, $byRefUses), $throwPoints]; + } + + /** + * The walks of processByRefInvocation() from the entry scope: the body's + * exit scope and throw points, in the closure's own scope. + * + * @param callable(MutatingScope): MutatingScope $enter + * @param Node\ClosureUse[] $byRefUses + * @return array{MutatingScope, list} + */ + private function walkByRefInvocation( + NodeScopeResolver $nodeScopeResolver, + Expr\Closure $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + MutatingScope $entryScope, + callable $enter, + array $byRefUses, + bool $untilFixpoint, + ): array + { $exitScope = null; $throwPoints = []; $count = 0; @@ -445,10 +499,7 @@ public function processByRefInvocation( } $exitScope = $exitScope === null ? $passExitScope : $exitScope->mergeWith($passExitScope); foreach ($result->getThrowPoints() as $throwPoint) { - $throwScope = $this->assignByRefUses($scope, $throwPoint->getScope(), $byRefUses); - $throwPoints[] = $throwPoint->isExplicit() - ? InternalThrowPoint::createExplicit($throwScope, $throwPoint->getType(), $call, $throwPoint->canContainAnyThrowable()) - : InternalThrowPoint::createImplicit($throwScope, $call); + $throwPoints[] = $throwPoint; } if (!$untilFixpoint) { break; @@ -471,7 +522,7 @@ public function processByRefInvocation( $exitScope = $exitScope->mergeWith($entryScope); } - return [$this->assignByRefUses($scope, $exitScope, $byRefUses), $throwPoints]; + return [$exitScope, $throwPoints]; } /** diff --git a/src/Analyser/ExpressionResultStorage.php b/src/Analyser/ExpressionResultStorage.php index 15d4a520915..a3de0d9a208 100644 --- a/src/Analyser/ExpressionResultStorage.php +++ b/src/Analyser/ExpressionResultStorage.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\Analyser\Generics\TemplateArgumentFrame; use PHPStan\Turbo\ShadowedByTurboExtension; use SplObjectStorage; @@ -19,6 +20,17 @@ final class ExpressionResultStorage */ private ?self $fallback = null; + /** + * The walks of closure bodies run for invocations of closures whose by-ref + * uses are followed (see ClosureProcessor::processByRefInvocation()), + * keyed by the closure: [frame, until fixpoint, native types promoted, + * entry scope, exit scope, throw points]. The walk is a function of these, + * so an invocation entering the body in a state already walked reads it. + * + * @var SplObjectStorage}>>|null + */ + private ?SplObjectStorage $byRefInvocationWalks = null; + public function __construct() { $this->exprResults = new SplObjectStorage(); @@ -46,4 +58,30 @@ public function findExpressionResult(Expr $expr): ?ExpressionResult return $this->exprResults[$expr] ?? ($this->fallback !== null ? $this->fallback->findExpressionResult($expr) : null); } + /** + * @param array{TemplateArgumentFrame|null, bool, bool, MutatingScope, MutatingScope, list} $walk + */ + public function storeByRefInvocationWalk(Expr\Closure $closure, array $walk): void + { + $this->byRefInvocationWalks ??= new SplObjectStorage(); + $walks = $this->byRefInvocationWalks[$closure] ?? []; + $walks[] = $walk; + $this->byRefInvocationWalks[$closure] = $walks; + } + + /** + * The walks stored here and in the storages this one falls back to. + * + * @return list}> + */ + public function findByRefInvocationWalks(Expr\Closure $closure): array + { + $walks = $this->byRefInvocationWalks[$closure] ?? []; + if ($this->fallback === null) { + return $walks; + } + + return [...$walks, ...$this->fallback->findByRefInvocationWalks($closure)]; + } + } diff --git a/turbo-ext/src/ClosureProcessor.cpp b/turbo-ext/src/ClosureProcessor.cpp index 9f56650eedc..20a0b041491 100644 --- a/turbo-ext/src/ClosureProcessor.cpp +++ b/turbo-ext/src/ClosureProcessor.cpp @@ -343,6 +343,89 @@ class ClosureProcessor zv::Val stmtsHold = zv::Val::copyOf(zv::Ref(stmts)); zv::Val entryScope = enter(scope); if (UNEXPECTED(entryScope.isUndef())) return zv::Val(); + zv::Val frame = pt_mutating_scope_get_current_template_argument_frame(Z_OBJ_P(scope)); + if (UNEXPECTED(frame.isUndef())) return zv::Val(); + bool nativeTypesPromoted; + if (UNEXPECTED(!pt_mutating_scope_native_types_promoted(Z_OBJ_P(scope), nativeTypesPromoted))) return zv::Val(); + // the body entered in a state it was already walked from, in the same + // pass: a closure invoked many times mostly re-enters one of a few states + zv::Val walk; + { + zv::Val storedWalks = pt_expression_result_storage_find_by_ref_invocation_walks(storage, expr); + if (UNEXPECTED(storedWalks.isUndef())) return zv::Val(); + for (zv::ArrayEntry entry : zv::ArrRef(storedWalks.raw())) { + zval *storedWalk = entry.value().deref().raw(); + zval *walkFrame = zend_hash_index_find(Z_ARRVAL_P(storedWalk), 0); + zval *walkUntilFixpoint = zend_hash_index_find(Z_ARRVAL_P(storedWalk), 1); + zval *walkNativeTypesPromoted = zend_hash_index_find(Z_ARRVAL_P(storedWalk), 2); + zval *walkEntryScope = zend_hash_index_find(Z_ARRVAL_P(storedWalk), 3); + if (UNEXPECTED(walkFrame == NULL || walkUntilFixpoint == NULL || walkNativeTypesPromoted == NULL || walkEntryScope == NULL)) continue; + bool sameFrame = Z_TYPE_P(walkFrame) == IS_OBJECT + ? Z_TYPE_P(frame.raw()) == IS_OBJECT && Z_OBJ_P(walkFrame) == Z_OBJ_P(frame.raw()) + : Z_TYPE_P(frame.raw()) != IS_OBJECT; + if (!sameFrame || (Z_TYPE_P(walkUntilFixpoint) == IS_TRUE) != untilFixpoint || (Z_TYPE_P(walkNativeTypesPromoted) == IS_TRUE) != nativeTypesPromoted) continue; + bool equals = false; + if (UNEXPECTED(!pt_mutating_scope_equals(Z_OBJ_P(walkEntryScope), Z_OBJ_P(entryScope.raw()), equals))) return zv::Val(); + if (!equals) continue; + walk = zv::Val::copyOf(zv::Ref(storedWalk)); + break; + } + } + if (walk.isUndef()) { + zv::Val walked = walkByRefInvocation(nodeScopeResolver, expr, scope, storage, entryScope.raw(), enter, byRefUses.raw(), stmtsHold.raw(), untilFixpoint); + if (UNEXPECTED(walked.isUndef())) return zv::Val(); + zv::Arr stored = zv::Arr::create(6); + stored.push(zv::Val::copyOf(zv::Ref(frame.raw()))); + stored.push(zv::Val::boolean(untilFixpoint)); + stored.push(zv::Val::boolean(nativeTypesPromoted)); + stored.push(zv::Val::copyOf(zv::Ref(entryScope.raw()))); + stored.push(zv::Val::copyOf(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(walked.raw()), 0)))); + stored.push(zv::Val::copyOf(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(walked.raw()), 1)))); + walk = zv::Val(std::move(stored)); + if (UNEXPECTED(!pt_expression_result_storage_store_by_ref_invocation_walk(storage, expr, walk.raw()))) return zv::Val(); + } + zval *exitScope = zend_hash_index_find(Z_ARRVAL_P(walk.raw()), 4); + zval *bodyThrowPoints = zend_hash_index_find(Z_ARRVAL_P(walk.raw()), 5); + if (UNEXPECTED(exitScope == NULL || bodyThrowPoints == NULL)) return zv::Val(); + + zv::Arr throwPoints = zv::Arr::empty(); + for (zv::ArrayEntry entry : zv::ArrRef(bodyThrowPoints)) { + zval *throwPoint = entry.value().deref().raw(); + zv::Val throwPointScopeHold; + zval *throwPointScope = pt_internal_throw_point_scope(throwPoint, throwPointScopeHold); + if (UNEXPECTED(throwPointScope == NULL)) return zv::Val(); + zv::Val throwScope = assignByRefUses(scope, throwPointScope, byRefUses.raw()); + if (UNEXPECTED(throwScope.isUndef())) return zv::Val(); + bool isExplicit; + if (UNEXPECTED(!pt_internal_throw_point_is_explicit(throwPoint, isExplicit))) return zv::Val(); + zv::Val created; + if (isExplicit) { + zv::Val typeHold; + zval *type = pt_internal_throw_point_type(throwPoint, typeHold); + if (UNEXPECTED(type == NULL)) return zv::Val(); + bool canContainAnyThrowable; + if (UNEXPECTED(!pt_internal_throw_point_can_contain_any_throwable(throwPoint, canContainAnyThrowable))) return zv::Val(); + created = pt_internal_throw_point_create_explicit(throwScope.raw(), type, call, canContainAnyThrowable); + } else { + created = pt_internal_throw_point_create_implicit(throwScope.raw(), call); + } + if (UNEXPECTED(created.isUndef())) return zv::Val(); + throwPoints.push(std::move(created)); + } + + zv::Val resultScope = assignByRefUses(scope, exitScope, byRefUses.raw()); + if (UNEXPECTED(resultScope.isUndef())) return zv::Val(); + zv::Arr pair = zv::Arr::create(2); + pair.push(std::move(resultScope)); + pair.push(std::move(throwPoints)); + return zv::Val(std::move(pair)); + } + + /* Mirrors walkByRefInvocation(): [the body's exit scope, its throw points] */ + template + zv::Val walkByRefInvocation(zval *nodeScopeResolver, zval *expr, zval *scope, zval *storage, zval *firstEntryScope, Enter &enter, zval *byRefUses, zval *stmts, bool untilFixpoint) const + { + zv::Val entryScope = zv::Val::copyOf(zv::Ref(firstEntryScope)); zv::Val exitScope; zv::Arr throwPoints = zv::Arr::empty(); zend_long count = 0; @@ -353,7 +436,7 @@ class ClosureProcessor if (UNEXPECTED(noop.isUndef())) return zv::Val(); zv::Val context = pt_statement_context_create_top_level(false); if (UNEXPECTED(context.isUndef())) return zv::Val(); - zv::Val result = pt_node_scope_resolver_process_stmt_nodes_internal(nodeScopeResolver, expr, stmtsHold.raw(), entryScope.raw(), passStorage.raw(), noop.raw(), context.raw()); + zv::Val result = pt_node_scope_resolver_process_stmt_nodes_internal(nodeScopeResolver, expr, stmts, entryScope.raw(), passStorage.raw(), noop.raw(), context.raw()); if (UNEXPECTED(result.isUndef())) return zv::Val(); zv::Val passExitScope; { @@ -386,35 +469,15 @@ class ClosureProcessor if (UNEXPECTED(resultThrowPoints == NULL)) return zv::Val(); zv::Val throwPointsIterated = zv::Val::copyOf(zv::Ref(resultThrowPoints)); for (zv::ArrayEntry entry : zv::ArrRef(throwPointsIterated.raw())) { - zval *throwPoint = entry.value().deref().raw(); - zv::Val throwPointScopeHold; - zval *throwPointScope = pt_internal_throw_point_scope(throwPoint, throwPointScopeHold); - if (UNEXPECTED(throwPointScope == NULL)) return zv::Val(); - zv::Val throwScope = assignByRefUses(scope, throwPointScope, byRefUses.raw()); - if (UNEXPECTED(throwScope.isUndef())) return zv::Val(); - bool isExplicit; - if (UNEXPECTED(!pt_internal_throw_point_is_explicit(throwPoint, isExplicit))) return zv::Val(); - zv::Val created; - if (isExplicit) { - zv::Val typeHold; - zval *type = pt_internal_throw_point_type(throwPoint, typeHold); - if (UNEXPECTED(type == NULL)) return zv::Val(); - bool canContainAnyThrowable; - if (UNEXPECTED(!pt_internal_throw_point_can_contain_any_throwable(throwPoint, canContainAnyThrowable))) return zv::Val(); - created = pt_internal_throw_point_create_explicit(throwScope.raw(), type, call, canContainAnyThrowable); - } else { - created = pt_internal_throw_point_create_implicit(throwScope.raw(), call); - } - if (UNEXPECTED(created.isUndef())) return zv::Val(); - throwPoints.push(std::move(created)); + throwPoints.push(entry.value().deref()); } } if (!untilFixpoint) break; // the next run starts from any state a run may have left - zv::Val entryAssigned = assignByRefUses(scope, entryScope.raw(), byRefUses.raw()); + zv::Val entryAssigned = assignByRefUses(scope, entryScope.raw(), byRefUses); if (UNEXPECTED(entryAssigned.isUndef())) return zv::Val(); - zv::Val exitAssigned = assignByRefUses(scope, passExitScope.raw(), byRefUses.raw()); + zv::Val exitAssigned = assignByRefUses(scope, passExitScope.raw(), byRefUses); if (UNEXPECTED(exitAssigned.isUndef())) return zv::Val(); zv::Val joined = pt_mutating_scope_merge_with(Z_OBJ_P(entryAssigned.raw()), exitAssigned.raw()); if (UNEXPECTED(joined.isUndef())) return zv::Val(); @@ -439,10 +502,8 @@ class ClosureProcessor exitScope = std::move(merged); } - zv::Val resultScope = assignByRefUses(scope, exitScope.raw(), byRefUses.raw()); - if (UNEXPECTED(resultScope.isUndef())) return zv::Val(); zv::Arr pair = zv::Arr::create(2); - pair.push(std::move(resultScope)); + pair.push(std::move(exitScope)); pair.push(std::move(throwPoints)); return zv::Val(std::move(pair)); } diff --git a/turbo-ext/src/ExpressionResultStorage.cpp b/turbo-ext/src/ExpressionResultStorage.cpp index 0d0bcd48f2f..69a85909eb8 100644 --- a/turbo-ext/src/ExpressionResultStorage.cpp +++ b/turbo-ext/src/ExpressionResultStorage.cpp @@ -25,6 +25,7 @@ namespace sigs = ptdecl::ExpressionResultStorage::sig; #include "zv.h" #define PT_ERS_PROP_FALLBACK 2 +#define PT_ERS_PROP_BY_REF_INVOCATION_WALKS 3 zend_class_entry *pt_ce_expression_result_storage = nullptr; @@ -82,6 +83,53 @@ class ExpressionResultStorage } } + /* Mirrors storeByRefInvocationWalk(): the walks keyed by the closure's + * handle as [closure, list of walks] - the closure is pinned like the + * stored Exprs */ + void storeByRefInvocationWalk(zval *closure, zval *walk) + { + zend_ulong id = Z_OBJ_HANDLE_P(closure); + zv::ObjRef obj(self); + zv::ArrRef table(obj.propAt(PT_ERS_PROP_BY_REF_INVOCATION_WALKS).raw()); + zv::Ref entry = table.findIndex(id); + zv::Arr walks = zv::Arr::empty(); + if (entry.raw() != NULL) { + zval *stored = zend_hash_index_find(Z_ARRVAL_P(entry.deref().raw()), 1); + if (stored != NULL) walks = zv::Arr::copyOfTable(Z_ARRVAL_P(stored)); + } + walks.push(zv::Val::copyOf(zv::Ref(walk))); + zv::Arr pair = zv::Arr::create(2); + pair.push(zv::Val::copyOf(zv::Ref(closure))); + pair.push(std::move(walks)); + zv::Val pairHold(std::move(pair)); + table.setIndex(id, zv::Ref(pairHold.raw())); + } + + /* Mirrors findByRefInvocationWalks(): this storage's walks, then the + * fallback chain's */ + zv::Val findByRefInvocationWalks(zval *closure) const + { + zend_ulong id = Z_OBJ_HANDLE_P(closure); + zv::Arr walks = zv::Arr::empty(); + zval *cur = self; + for (;;) { + zv::ObjRef obj(cur); + zv::Ref entry = zv::ArrRef(obj.propAt(PT_ERS_PROP_BY_REF_INVOCATION_WALKS).raw()).findIndex(id); + if (entry.raw() != NULL) { + zval *stored = zend_hash_index_find(Z_ARRVAL_P(entry.deref().raw()), 1); + if (stored != NULL) { + for (zv::ArrayEntry walk : zv::ArrRef(stored)) { + walks.push(zv::Val::copyOf(walk.value())); + } + } + } + zval *fallback = obj.propAt(PT_ERS_PROP_FALLBACK).raw(); + if (Z_TYPE_P(fallback) != IS_OBJECT) break; + cur = fallback; + } + return zv::Val(std::move(walks)); + } + private: zval *self; }; @@ -115,6 +163,9 @@ PT_MINIT_REGISTRATION(pt_register_expression_result_storage) cls.privateArrayProperty("exprsById"); cls.privateArrayProperty("resultsById"); cls.property("fallback", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self"); + /* the twin's ?SplObjectStorage $byRefInvocationWalks: [closure, walks] + * keyed by the closure's handle */ + cls.privateArrayProperty("byRefInvocationWalksById"); /* the twin's constructor only initialized its SplObjectStorage; the * native property defaults already cover that */ @@ -163,6 +214,27 @@ PT_MINIT_REGISTRATION(pt_register_expression_result_storage) ExpressionResultStorage(ZEND_THIS).findExpressionResult(expr).intoReturnValue(return_value); }); + cls.method(sigs::storeByRefInvocationWalk, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *closure, *walk; + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + if (UNEXPECTED(closureCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(closure, closureCe) + Z_PARAM_ARRAY(walk) + ZEND_PARSE_PARAMETERS_END(); + ExpressionResultStorage(ZEND_THIS).storeByRefInvocationWalk(closure, walk); + }); + + cls.method(sigs::findByRefInvocationWalks, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *closure; + zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); + if (UNEXPECTED(closureCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(closure, closureCe) + ZEND_PARSE_PARAMETERS_END(); + ExpressionResultStorage(ZEND_THIS).findByRefInvocationWalks(closure).intoReturnValue(return_value); + }); + cls.shadow(&pt_ce_expression_result_storage); } @@ -203,3 +275,19 @@ bool pt_expression_result_storage_merge_results(zval *storage, zval *other) } /* }}} */ + +zv::Val pt_expression_result_storage_find_by_ref_invocation_walks(zval *storage, zval *closure) +{ + if (EXPECTED(Z_OBJCE_P(storage) == pt_ce_expression_result_storage)) return ExpressionResultStorage(storage).findByRefInvocationWalks(closure); + return pt_type_call(Z_OBJ_P(storage), "findbyrefinvocationwalks", sizeof("findbyrefinvocationwalks") - 1, 1, closure); +} + +[[nodiscard]] bool pt_expression_result_storage_store_by_ref_invocation_walk(zval *storage, zval *closure, zval *walk) +{ + if (EXPECTED(Z_OBJCE_P(storage) == pt_ce_expression_result_storage)) { + ExpressionResultStorage(storage).storeByRefInvocationWalk(closure, walk); + return true; + } + zv::Args argv{closure, walk}; + return !pt_type_call(Z_OBJ_P(storage), "storebyrefinvocationwalk", sizeof("storebyrefinvocationwalk") - 1, 2, argv).isUndef(); +} diff --git a/turbo-ext/src/generated/ClosureProcessor.h b/turbo-ext/src/generated/ClosureProcessor.h index c382e861001..e0f9c493e0c 100644 --- a/turbo-ext/src/generated/ClosureProcessor.h +++ b/turbo-ext/src/generated/ClosureProcessor.h @@ -79,27 +79,30 @@ inline constexpr char strings[] = "creationScope\0" /* 963 */ "untilFixpoint\0" /* 977 */ "processByRefInvocation\0" /* 991 */ - "byRefEntryTypes\0" /* 1014 */ - "processDeferredByRefClosureBody\0" /* 1030 */ - "callableParameters\0" /* 1062 */ - "enterWithCapturedUses\0" /* 1081 */ - "source\0" /* 1103 */ - "byRefUses\0" /* 1110 */ - "assignByRefUses\0" /* 1120 */ - "closureScope\0" /* 1136 */ - "gatheredReturnStatementsWithScope\0" /* 1149 */ - "gatheredYieldStatementsWithScope\0" /* 1183 */ - "executionEnds\0" /* 1216 */ - "throwPoints\0" /* 1230 */ - "impurePoints\0" /* 1242 */ - "invalidateExpressions\0" /* 1255 */ - "refineClosureNodeScope\0" /* 1277 */ - "invalidatedExpressions\0" /* 1300 */ - "uses\0" /* 1323 */ - "processImmediatelyCalledCallable\0" /* 1328 */ - "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 1361 */ - "PHPStan\\Analyser\\ProcessArrowFunctionResult\0" /* 1395 */ - "processArrowFunctionNode"; /* 1439 */ + "entryScope\0" /* 1014 */ + "enter\0" /* 1025 */ + "byRefUses\0" /* 1031 */ + "walkByRefInvocation\0" /* 1041 */ + "byRefEntryTypes\0" /* 1061 */ + "processDeferredByRefClosureBody\0" /* 1077 */ + "callableParameters\0" /* 1109 */ + "enterWithCapturedUses\0" /* 1128 */ + "source\0" /* 1150 */ + "assignByRefUses\0" /* 1157 */ + "closureScope\0" /* 1173 */ + "gatheredReturnStatementsWithScope\0" /* 1186 */ + "gatheredYieldStatementsWithScope\0" /* 1220 */ + "executionEnds\0" /* 1253 */ + "throwPoints\0" /* 1267 */ + "impurePoints\0" /* 1279 */ + "invalidateExpressions\0" /* 1292 */ + "refineClosureNodeScope\0" /* 1314 */ + "invalidatedExpressions\0" /* 1337 */ + "uses\0" /* 1360 */ + "processImmediatelyCalledCallable\0" /* 1365 */ + "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 1398 */ + "PHPStan\\Analyser\\ProcessArrowFunctionResult\0" /* 1432 */ + "processArrowFunctionNode"; /* 1476 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 10), /* __construct $container */ reg::packed(48, 0, 72), /* __construct $expressionResultFactory */ @@ -137,47 +140,56 @@ inline constexpr reg::PackedArg args[] = { reg::packed(963, 0, 649), /* processByRefInvocation $creationScope */ reg::packed(977, MAY_BE_BOOL), /* processByRefInvocation $untilFixpoint */ reg::packed(471, MAY_BE_ARRAY), /* processByRefInvocation return */ + reg::packed(532, 0, 550), /* walkByRefInvocation $nodeScopeResolver */ + reg::packed(610, 0, 615), /* walkByRefInvocation $expr */ + reg::packed(643, 0, 649), /* walkByRefInvocation $scope */ + reg::packed(680, 0, 688), /* walkByRefInvocation $storage */ + reg::packed(1014, 0, 649), /* walkByRefInvocation $entryScope */ + reg::packed(1025, MAY_BE_CALLABLE), /* walkByRefInvocation $enter */ + reg::packed(1031, MAY_BE_ARRAY), /* walkByRefInvocation $byRefUses */ + reg::packed(977, MAY_BE_BOOL), /* walkByRefInvocation $untilFixpoint */ + reg::packed(471, MAY_BE_ARRAY), /* walkByRefInvocation return */ reg::packed(532, 0, 550), /* processDeferredByRefClosureBody $nodeScopeResolver */ reg::packed(610, 0, 615), /* processDeferredByRefClosureBody $expr */ reg::packed(643, 0, 649), /* processDeferredByRefClosureBody $scope */ reg::packed(680, 0, 688), /* processDeferredByRefClosureBody $storage */ reg::packed(729, MAY_BE_CALLABLE), /* processDeferredByRefClosureBody $nodeCallback */ - reg::packed(1014, MAY_BE_ARRAY), /* processDeferredByRefClosureBody $byRefEntryTypes */ + reg::packed(1061, MAY_BE_ARRAY), /* processDeferredByRefClosureBody $byRefEntryTypes */ reg::packed(471, MAY_BE_VOID), /* processDeferredByRefClosureBody return */ reg::packed(643, 0, 649), /* enterWithCapturedUses $scope */ reg::packed(610, 0, 615), /* enterWithCapturedUses $expr */ - reg::packed(1062, MAY_BE_ARRAY), /* enterWithCapturedUses $callableParameters */ + reg::packed(1109, MAY_BE_ARRAY), /* enterWithCapturedUses $callableParameters */ reg::packed(963, 0, 649), /* enterWithCapturedUses $creationScope */ reg::packed(471, 0, 649), /* enterWithCapturedUses return */ reg::packed(643, 0, 649), /* assignByRefUses $scope */ - reg::packed(1103, 0, 649), /* assignByRefUses $source */ - reg::packed(1110, MAY_BE_ARRAY), /* assignByRefUses $byRefUses */ + reg::packed(1150, 0, 649), /* assignByRefUses $source */ + reg::packed(1031, MAY_BE_ARRAY), /* assignByRefUses $byRefUses */ reg::packed(471, 0, 649), /* assignByRefUses return */ - reg::packed(1136, 0, 649), /* refineClosureNodeScope $closureScope */ + reg::packed(1173, 0, 649), /* refineClosureNodeScope $closureScope */ reg::packed(643, 0, 649), /* refineClosureNodeScope $scope */ reg::packed(610, 0, 615), /* refineClosureNodeScope $expr */ - reg::packed(1149, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredReturnStatementsWithScope */ - reg::packed(1183, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredYieldStatementsWithScope */ - reg::packed(1216, MAY_BE_ARRAY), /* refineClosureNodeScope $executionEnds */ - reg::packed(1230, MAY_BE_ARRAY), /* refineClosureNodeScope $throwPoints */ - reg::packed(1242, MAY_BE_ARRAY), /* refineClosureNodeScope $impurePoints */ - reg::packed(1255, MAY_BE_ARRAY), /* refineClosureNodeScope $invalidateExpressions */ + reg::packed(1186, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredReturnStatementsWithScope */ + reg::packed(1220, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredYieldStatementsWithScope */ + reg::packed(1253, MAY_BE_ARRAY), /* refineClosureNodeScope $executionEnds */ + reg::packed(1267, MAY_BE_ARRAY), /* refineClosureNodeScope $throwPoints */ + reg::packed(1279, MAY_BE_ARRAY), /* refineClosureNodeScope $impurePoints */ + reg::packed(1292, MAY_BE_ARRAY), /* refineClosureNodeScope $invalidateExpressions */ reg::packed(680, 0, 688), /* refineClosureNodeScope $storage */ reg::packed(471, 0, 649), /* refineClosureNodeScope return */ reg::packed(643, 0, 649), /* processImmediatelyCalledCallable $scope */ - reg::packed(1300, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $invalidatedExpressions */ - reg::packed(1323, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $uses */ + reg::packed(1337, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $invalidatedExpressions */ + reg::packed(1360, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $uses */ reg::packed(471, 0, 649), /* processImmediatelyCalledCallable return */ reg::packed(532, 0, 550), /* processArrowFunctionNode $nodeScopeResolver */ reg::packed(585, 0, 590), /* processArrowFunctionNode $stmt */ - reg::packed(610, 0, 1361), /* processArrowFunctionNode $expr */ + reg::packed(610, 0, 1398), /* processArrowFunctionNode $expr */ reg::packed(643, 0, 649), /* processArrowFunctionNode $scope */ reg::packed(680, 0, 688), /* processArrowFunctionNode $storage */ reg::packed(729, MAY_BE_CALLABLE), /* processArrowFunctionNode $nodeCallback */ reg::packed(785, MAY_BE_NULL, 798), /* processArrowFunctionNode $passedToType */ reg::packed(816, MAY_BE_NULL, 798, false, false, 835), /* processArrowFunctionNode $nativePassedToType */ reg::packed(742, MAY_BE_NULL, 750, false, false, 835), /* processArrowFunctionNode $context */ - reg::packed(471, 0, 1395), /* processArrowFunctionNode return */ + reg::packed(471, 0, 1432), /* processArrowFunctionNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -189,12 +201,13 @@ inline constexpr sigtab::Sig getParametersProcessor = { { 509 /* getParametersPr inline constexpr sigtab::Sig processClosureNode = { { 878 /* processClosureNode */, 8, 7, 9, 16, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig processClosureNodeInternal = { { 897 /* processClosureNodeInternal */, 8, 17, 9, 26, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig processByRefInvocation = { { 991 /* processByRefInvocation */, 8, 27, 8, 35, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processDeferredByRefClosureBody = { { 1030 /* processDeferredByRefClosureBody */, 6, 36, 6, 42, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterWithCapturedUses = { { 1081 /* enterWithCapturedUses */, 4, 43, 4, 47, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig assignByRefUses = { { 1120 /* assignByRefUses */, 3, 48, 3, 51, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig refineClosureNodeScope = { { 1277 /* refineClosureNodeScope */, 10, 52, 10, 62, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processImmediatelyCalledCallable = { { 1328 /* processImmediatelyCalledCallable */, 3, 63, 3, 66, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processArrowFunctionNode = { { 1439 /* processArrowFunctionNode */, 7, 67, 9, 76, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig walkByRefInvocation = { { 1041 /* walkByRefInvocation */, 8, 36, 8, 44, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processDeferredByRefClosureBody = { { 1077 /* processDeferredByRefClosureBody */, 6, 45, 6, 51, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterWithCapturedUses = { { 1128 /* enterWithCapturedUses */, 4, 52, 4, 56, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig assignByRefUses = { { 1157 /* assignByRefUses */, 3, 57, 3, 60, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig refineClosureNodeScope = { { 1314 /* refineClosureNodeScope */, 10, 61, 10, 71, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processImmediatelyCalledCallable = { { 1365 /* processImmediatelyCalledCallable */, 3, 72, 3, 75, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processArrowFunctionNode = { { 1476 /* processArrowFunctionNode */, 7, 76, 9, 85, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ClosureProcessor diff --git a/turbo-ext/src/generated/ExpressionResultStorage.h b/turbo-ext/src/generated/ExpressionResultStorage.h index 5aa8674d8fe..ca735826a60 100644 --- a/turbo-ext/src/generated/ExpressionResultStorage.h +++ b/turbo-ext/src/generated/ExpressionResultStorage.h @@ -12,6 +12,7 @@ namespace ptdecl::ExpressionResultStorage { namespace slot { inline constexpr uint32_t exprResults = 0; inline constexpr uint32_t fallback = 1; +inline constexpr uint32_t byRefInvocationWalks = 2; } // namespace slot inline void declareClass(reg::Class &cls) @@ -24,6 +25,7 @@ inline void declareProperties(reg::Class &cls) { cls.property("exprResults", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "SplObjectStorage"); cls.property("fallback", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\ExpressionResultStorage"); + cls.property("byRefInvocationWalks", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "SplObjectStorage"); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -40,7 +42,12 @@ inline constexpr char strings[] = "expressionResult\0" /* 108 */ "PHPStan\\Analyser\\ExpressionResult\0" /* 125 */ "storeExpressionResult\0" /* 159 */ - "findExpressionResult"; /* 181 */ + "findExpressionResult\0" /* 181 */ + "closure\0" /* 202 */ + "PhpParser\\Node\\Expr\\Closure\0" /* 210 */ + "walk\0" /* 238 */ + "storeByRefInvocationWalk\0" /* 243 */ + "findByRefInvocationWalks"; /* 268 */ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 13), /* duplicate return */ reg::packed(64, 0, 13), /* mergeResults $other */ @@ -50,6 +57,11 @@ inline constexpr reg::PackedArg args[] = { reg::packed(12, MAY_BE_VOID), /* storeExpressionResult return */ reg::packed(83, 0, 88), /* findExpressionResult $expr */ reg::packed(12, MAY_BE_NULL, 125), /* findExpressionResult return */ + reg::packed(202, 0, 210), /* storeByRefInvocationWalk $closure */ + reg::packed(238, MAY_BE_ARRAY), /* storeByRefInvocationWalk $walk */ + reg::packed(12, MAY_BE_VOID), /* storeByRefInvocationWalk return */ + reg::packed(202, 0, 210), /* findByRefInvocationWalks $closure */ + reg::packed(12, MAY_BE_ARRAY), /* findByRefInvocationWalks return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -61,6 +73,8 @@ inline constexpr sigtab::Sig duplicate = { { 54 /* duplicate */, 0, 0, 0, 0, ZEN inline constexpr sigtab::Sig mergeResults = { { 70 /* mergeResults */, 1, 1, 1, 2, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig storeExpressionResult = { { 159 /* storeExpressionResult */, 2, 3, 2, 5, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig findExpressionResult = { { 181 /* findExpressionResult */, 1, 6, 1, 7, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig storeByRefInvocationWalk = { { 243 /* storeByRefInvocationWalk */, 2, 8, 2, 10, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig findByRefInvocationWalks = { { 268 /* findByRefInvocationWalks */, 1, 11, 1, 12, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ExpressionResultStorage diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 96cf47dd3b5..01a4d3f63a2 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -1556,6 +1556,10 @@ extern zend_class_entry *pt_ce_volatile_expression_helper; zv::Val pt_expression_result_storage_new(); zv::Val pt_expression_result_storage_find(zval *storage, zval *expr); zv::Val pt_expression_result_storage_duplicate(zval *storage); +/* ExpressionResultStorage::findByRefInvocationWalks() / storeByRefInvocationWalk() + * (false = pending exception) */ +zv::Val pt_expression_result_storage_find_by_ref_invocation_walks(zval *storage, zval *closure); +[[nodiscard]] bool pt_expression_result_storage_store_by_ref_invocation_walk(zval *storage, zval *closure, zval *walk); /* the shadowing ExpressionResultStorageStack (ExpressionResultStorageStack.cpp) * — $stack->getCurrent(): the native body for a native stack, the method for * anything else; UNDEF = pending exception */ diff --git a/turbo-ext/tests/signature-parity.php b/turbo-ext/tests/signature-parity.php index 0f10d7c642a..64ea91b8121 100644 --- a/turbo-ext/tests/signature-parity.php +++ b/turbo-ext/tests/signature-parity.php @@ -39,6 +39,7 @@ ], 'PHPStan\\Analyser\\ExpressionResultStorage' => [ '~^property (exprResults is not declared natively|(exprsById|resultsById) is declared natively but not in PHP)$~' => 'the result table is two id-keyed arrays instead of the twin\'s SplObjectStorage', + '~^property (byRefInvocationWalks is not declared natively|byRefInvocationWalksById is declared natively but not in PHP)$~' => 'the by-ref invocation walks are an id-keyed array of [closure, walks] instead of the twin\'s SplObjectStorage', ], ]; diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index cf60242c5f3..ee3479e68b4 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -378,6 +378,21 @@ function check(bool $cond, string $msg): void check($storage->findExpressionResult($exprC) === null, "ERS $label: mergeResults ignores the other's fallback chain"); check($storage->findExpressionResult($exprA) === $resultB, "ERS $label: mergeResults keeps existing entries"); + $closureA = new \PhpParser\Node\Expr\Closure(); + $closureB = new \PhpParser\Node\Expr\Closure(); + $walkA = [null, true, false, 'entryA', 'exitA', []]; + $walkB = [null, false, false, 'entryB', 'exitB', []]; + $walkC = [null, false, true, 'entryC', 'exitC', []]; + $walks = new $storageClass(); + check($walks->findByRefInvocationWalks($closureA) === [], "ERS $label: no by-ref walks stored"); + $walks->storeByRefInvocationWalk($closureA, $walkA); + $walks->storeByRefInvocationWalk($closureA, $walkB); + check($walks->findByRefInvocationWalks($closureA) === [$walkA, $walkB], "ERS $label: by-ref walks in the order stored"); + check($walks->findByRefInvocationWalks($closureB) === [], "ERS $label: by-ref walks are per closure"); + $walksChild = $walks->duplicate(); + $walksChild->storeByRefInvocationWalk($closureA, $walkC); + check($walksChild->findByRefInvocationWalks($closureA) === [$walkC, $walkA, $walkB], "ERS $label: own by-ref walks before the fallback chain's"); + check($walks->findByRefInvocationWalks($closureA) === [$walkA, $walkB], "ERS $label: by-ref walks stored on a duplicate do not leak back"); } From 0e5c2643c138b113e14771d8d0351bfa6b61a643 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 11:38:53 +0200 Subject: [PATCH 4/9] Replay the last walk inferring static variables as the second pass A body with `static` variables walked the statements from the first `static` one until their types converged, and then once more in the second pass with the converged types - which the last walk had just used. The `Enum` trait's `static $cache` methods did it for every enum using it. The last walk is made with the returned types and conditional expressions on every way out of the loop. When the body observed no template argument or closure signature (its constraints are empty - nothing to resolve, so a walk that observes computes what one that does not observe computes) and no closure or class follows the first `static` statement (the second pass walks their bodies resolving their own template arguments), the second pass replays that walk's recording and takes its results instead of walking. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- .../Generics/StaticVariableInference.php | 57 ++++ .../Generics/TemplateArgumentStats.php | 1 + src/Analyser/StatementsHandler.php | 90 ++++++- turbo-ext/src/StatementsHandler.cpp | 112 +++++++- turbo-ext/src/StaticVariableInference.cpp | 73 +++++ turbo-ext/src/generated/StatementsHandler.h | 252 ++++++++++-------- .../src/generated/StaticVariableInference.h | 95 ++++--- turbo-ext/src/support.h | 1 + 8 files changed, 513 insertions(+), 168 deletions(-) diff --git a/src/Analyser/Generics/StaticVariableInference.php b/src/Analyser/Generics/StaticVariableInference.php index e6b38e8e892..d7cb9ac1d57 100644 --- a/src/Analyser/Generics/StaticVariableInference.php +++ b/src/Analyser/Generics/StaticVariableInference.php @@ -46,6 +46,8 @@ final class StaticVariableInference private const SITES_ATTRIBUTE = 'staticVariableInferenceSites'; + private const LAST_FUNCTION_LIKE_ATTRIBUTE = 'staticVariableInferenceLastFunctionLike'; + public function __construct( private ReflectionProvider $reflectionProvider, #[AutowiredParameter(ref: '%featureToggles.staticVariablesFromUsages%')] @@ -79,6 +81,25 @@ public function getSites(Node $functionLike, array $stmts): array return $sites; } + /** + * Whether a statement from $index on holds a closure, an arrow function or + * a class: the walks inferring the `static` variables walk their bodies + * without resolving template arguments, unlike the second pass. + * + * @param Node\Stmt[] $stmts + */ + public function hasFunctionLikeFrom(Node $functionLike, array $stmts, int $index): bool + { + /** @var int|null $last */ + $last = $functionLike->getAttribute(self::LAST_FUNCTION_LIKE_ATTRIBUTE); + if ($last === null) { + $last = self::findLastFunctionLikeStatement($stmts); + $functionLike->setAttribute(self::LAST_FUNCTION_LIKE_ATTRIBUTE, $last); + } + + return $last >= $index; + } + /** Whether the current walk infers the type of the `static` variable. */ public function isInferred(MutatingScope $scope, Expr\Variable $var): bool { @@ -297,6 +318,42 @@ private static function scanSites(array $stmts): array return $sites; } + /** + * The index of the last statement holding a function-like or a class, -1 + * when none does. + * + * @param Node\Stmt[] $stmts + */ + private static function findLastFunctionLikeStatement(array $stmts): int + { + $last = -1; + foreach ($stmts as $index => $stmt) { + $stack = [$stmt]; + while (count($stack) > 0) { + $node = array_pop($stack); + if ($node instanceof Node\FunctionLike || $node instanceof Node\Stmt\ClassLike) { + $last = $index; + break; + } + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + $stack[] = $subNode; + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if (!$item instanceof Node) { + continue; + } + $stack[] = $item; + } + } + } + } + } + + return $last; + } + /** * @param array $names */ diff --git a/src/Analyser/Generics/TemplateArgumentStats.php b/src/Analyser/Generics/TemplateArgumentStats.php index 1ee422d6c02..89b20e386fd 100644 --- a/src/Analyser/Generics/TemplateArgumentStats.php +++ b/src/Analyser/Generics/TemplateArgumentStats.php @@ -58,6 +58,7 @@ final class TemplateArgumentStats 'closureObservationPasses' => 0, 'closureObservationStatements' => 0, 'closureObservationStatementsReplayed' => 0, + 'staticVariableWalksReplayed' => 0, ]; public static function enableFromEnvironment(): void diff --git a/src/Analyser/StatementsHandler.php b/src/Analyser/StatementsHandler.php index 87e159c04b7..42ddc16f191 100644 --- a/src/Analyser/StatementsHandler.php +++ b/src/Analyser/StatementsHandler.php @@ -64,6 +64,15 @@ final class StatementsHandler { + /** The last walk inferring the `static` variables walked a statement: [kind, recording from, recording to] */ + private const STATIC_WALK_WALKED = 0; + + /** It carried a statement's recorded effect over: [kind, statement index, 0] */ + private const STATIC_WALK_CARRIED = 1; + + /** It converged with the observation pass at a statement - the rest stands: [kind, statement index, 0] */ + private const STATIC_WALK_REST = 2; + private const MENTIONED_VARIABLES_ATTRIBUTE = 'templateArgumentMentionedVariables'; /** PHPSTAN_TEMPLATE_ARGUMENTS_DEBUG=1 prints every second-pass re-walk/replay decision. */ @@ -584,16 +593,24 @@ private function processBodyStmtNodesTwoPass( $staticVariableTypes = []; $staticVariableConditionalExpressions = []; $staticStatementIndexes = []; + $staticWalk = null; if ($staticSites !== []) { foreach ($staticSites as [, $index]) { $staticStatementIndexes[$index] = true; } - [$staticVariableTypes, $staticVariableConditionalExpressions, $constraints] = $this->inferStaticVariableTypes($nodeScopeResolver, $parentNode, $stmts, $staticSites, $staticStatementIndexes, $parentFrame, $entries, $recording, $storage, $observationContext); + [$staticVariableTypes, $staticVariableConditionalExpressions, $constraints, $staticWalk] = $this->inferStaticVariableTypes($nodeScopeResolver, $parentNode, $stmts, $staticSites, $staticStatementIndexes, $parentFrame, $entries, $recording, $storage, $observationContext); } $frame = $this->templateArgumentResolver->resolve($constraints, $parentFrame, $statementStartTokenPositions, $parentNode, $stmts); if ($staticVariableTypes !== []) { $frame = $frame->withStaticVariableTypes($staticVariableTypes, $staticStatementIndexes, $staticVariableConditionalExpressions); } + if ( + $staticWalk !== null + && $constraints->isEmpty() + && !$this->staticVariableInference->hasFunctionLikeFrom($parentNode, $stmts, $staticWalk[0]) + ) { + return $this->replayStaticVariableWalk($nodeScopeResolver, $stmts, $entries, $recording, $staticWalk, $frame, $parentFrame, $parentConstraints, $storage, $nodeCallback, $scope); + } if ($frame->isObservingClosures()) { $frame = $this->observeClosureSignatures($nodeScopeResolver, $parentNode, $stmts, $frame, $entries, $storage, $observationContext, $statementStartTokenPositions); } @@ -685,7 +702,8 @@ private function processBodyStmtNodesTwoPass( * statements that read a variable whose type changed - until the type at * each `static` statement holds every type the variable takes after it, * generalized like a loop's variables. The facts of the last walk are the - * ones the template arguments and closure signatures are resolved from. + * ones the template arguments and closure signatures are resolved from; the + * walk itself is returned too, see replayStaticVariableWalk(). * * The variables of a run of `static` statements (see * StaticVariableInference::getRuns()) hold one of the states a call left @@ -697,7 +715,7 @@ private function processBodyStmtNodesTwoPass( * @param non-empty-list $staticSites * @param array $staticStatementIndexes * @param array $entries - * @return array{array, array}>, TemplateArgumentConstraints} + * @return array{array, array}>, TemplateArgumentConstraints, array{int, StatementListWalkState, RecordingNodeCallback, ExpressionResultStorage, list}} */ private function inferStaticVariableTypes( NodeScopeResolver $nodeScopeResolver, @@ -741,6 +759,8 @@ private function inferStaticVariableTypes( $state->scope = $state->scope->withTemplateArgumentFrame($frame); $walkStorage = $storage->duplicate(); $walkRecording = new RecordingNodeCallback(); + /** @var list $walkLog see replayStaticVariableWalk() */ + $walkLog = []; /** @var list $replayedScopes */ $replayedScopes = []; /** @var list $carriedOverScopes the entry scopes of the statements not walked again */ @@ -758,6 +778,7 @@ private function inferStaticVariableTypes( // converged with the observation pass: the rest of it stands $state->scope = $this->withRecordedConstraints($state->scope, $recordedEntry->scope, $entries[$stmtCount][0]->scope); $this->appendRecordedStatementResults($state, $recordedEntry, $entries[$stmtCount][0]); + $walkLog[] = [self::STATIC_WALK_REST, $i, 0]; break; } @@ -768,13 +789,16 @@ private function inferStaticVariableTypes( || $frame->ownsSiteInStatement($i) || $this->statementMentionsAnyVariable($stmt, $differingRoots) ) { + $from = $walkRecording->count(); $this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $walkStorage, $walkRecording, $context, true); + $walkLog[] = [self::STATIC_WALK_WALKED, $from, $walkRecording->count()]; $replayedScopes[] = $state->scope; continue; } // the statement does not read what changed: the variables keep // their types through it + $walkLog[] = [self::STATIC_WALK_CARRIED, $i, 0]; $replayedScopes[] = $state->scope; $carriedOverScopes[] = $state->scope; $recordedExit = $entries[$i + 1][0]; @@ -847,7 +871,65 @@ private function inferStaticVariableTypes( $siteTypes[spl_object_id($var)] = [$var, $type, $nativeType]; } - return [$siteTypes, $conditionalExpressions, $constraints]; + // every way out of the loop leaves the last walk made with the types + // and conditional expressions returned + return [$siteTypes, $conditionalExpressions, $constraints, [$start, $state, $walkRecording, $walkStorage, $walkLog]]; + } + + /** + * The second pass of a body whose only sites are its `static` statements + * and which observed no template argument or closure signature: it walks + * the statements from the first `static` one with the resolved types, as + * the last walk inferring them did - with no marker to resolve, a walk + * that observes computes what one that does not observe computes. That + * walk's recording and results stand for it. A closure or class in these + * statements is walked differently by the second pass (resolving its own + * template arguments), so the second pass walks those bodies itself. + * + * @param Node\Stmt[] $stmts + * @param array $entries + * @param array{int, StatementListWalkState, RecordingNodeCallback, ExpressionResultStorage, list} $staticWalk + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function replayStaticVariableWalk( + NodeScopeResolver $nodeScopeResolver, + array $stmts, + array $entries, + RecordingNodeCallback $recording, + array $staticWalk, + TemplateArgumentFrame $frame, + ?TemplateArgumentFrame $parentFrame, + ?TemplateArgumentConstraints $parentConstraints, + ExpressionResultStorage $storage, + callable $nodeCallback, + MutatingScope $scope, + ): InternalStatementResult + { + [$start, $state, $walkRecording, $walkStorage, $walkLog] = $staticWalk; + $stmtCount = count($stmts); + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('bodiesWithSites'); + TemplateArgumentStats::increment('staticVariableWalksReplayed'); + } + $nodeScopeResolver->replayRecordingRange($recording, 0, $entries[$start][1], $nodeCallback, $storage, $scope); + $storage->mergeResults($walkStorage); + foreach ($walkLog as [$kind, $a, $b]) { + if ($kind === self::STATIC_WALK_WALKED) { + $nodeScopeResolver->replayRecordingRange($walkRecording, $a, $b, $nodeCallback, $storage, $scope); + continue; + } + if ($kind === self::STATIC_WALK_CARRIED) { + $nodeScopeResolver->replayRecordingRange($recording, $entries[$a][1], $entries[$a + 1][1], $nodeCallback, $storage, $scope); + continue; + } + $nodeScopeResolver->replayRecordingRange($recording, $entries[$a][1], $recording->count(), $nodeCallback, $storage, $scope); + // the recorded end scope, as the second pass takes it + $state->scope = $entries[$stmtCount][0]->scope; + } + $this->processDeferredByRefClosureBodies($nodeScopeResolver, $frame, $state, $storage, $nodeCallback); + + $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); + return $state->toResult(); } /** diff --git a/turbo-ext/src/StatementsHandler.cpp b/turbo-ext/src/StatementsHandler.cpp index cd0cbe3f9ba..520182d5a9f 100644 --- a/turbo-ext/src/StatementsHandler.cpp +++ b/turbo-ext/src/StatementsHandler.cpp @@ -48,6 +48,10 @@ zend_class_entry *pt_ce_statements_handler = nullptr; /* NodeScopeResolver::LOOP_SCOPE_ITERATIONS / ::GENERALIZE_AFTER_ITERATION */ #define PT_SH_LOOP_SCOPE_ITERATIONS_LIMIT 3 #define PT_SH_GENERALIZE_AFTER_ITERATION_LIMIT 1 +/* the twin's STATIC_WALK_* kinds of the last static-variable walk's log */ +#define PT_SH_STATIC_WALK_WALKED 0 +#define PT_SH_STATIC_WALK_CARRIED 1 +#define PT_SH_STATIC_WALK_REST 2 namespace { @@ -1391,6 +1395,7 @@ class StatementsHandler zv::Val staticVariableTypes = zv::Val(zv::Arr::empty()); zv::Val staticVariableConditionalExpressions = zv::Val(zv::Arr::empty()); zv::Arr staticStatementIndexes = zv::Arr::empty(); + zv::Val staticWalk = zv::Val::null(); if (staticSites.ref().isArray() && zend_hash_num_elements(Z_ARRVAL_P(staticSites.raw())) > 0) { for (auto entry : zv::ArrRef(staticSites.raw())) { zval *index = zend_hash_index_find(Z_ARRVAL_P(entry.value().deref().raw()), 1); @@ -1405,6 +1410,7 @@ class StatementsHandler staticVariableTypes = zv::Val::copyOf(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(inferred.raw()), 0))); staticVariableConditionalExpressions = zv::Val::copyOf(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(inferred.raw()), 1))); finalConstraints = zv::Val::copyOf(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(inferred.raw()), 2))); + staticWalk = zv::Val::copyOf(zv::Ref(zend_hash_index_find(Z_ARRVAL_P(inferred.raw()), 3))); } zv::Val frame = templateArgumentResolverResolve(OBJ_PROP_NUM(self, slots::templateArgumentResolver), finalConstraints.raw(), parentFrame.raw(), statementStartTokenPositions.raw(), parentNode, stmts); if (UNEXPECTED(frame.isUndef())) return zv::Val(); @@ -1412,6 +1418,18 @@ class StatementsHandler frame = pt_template_argument_frame_with_static_variable_types(frame.raw(), staticVariableTypes.raw(), staticStatementIndexes.raw(), staticVariableConditionalExpressions.raw()); if (UNEXPECTED(frame.isUndef())) return zv::Val(); } + if (staticWalk.ref().isArray()) { + bool constraintsEmpty; + if (UNEXPECTED(!pt_template_argument_constraints_is_empty(finalConstraints.raw(), constraintsEmpty))) return zv::Val(); + if (constraintsEmpty) { + bool hasFunctionLike; + zend_long start = zval_get_long(zend_hash_index_find(Z_ARRVAL_P(staticWalk.raw()), 0)); + if (UNEXPECTED(!pt_static_variable_inference_has_function_like_from(OBJ_PROP_NUM(self, slots::staticVariableInference), parentNode, stmts, start, hasFunctionLike))) return zv::Val(); + if (!hasFunctionLike) { + return replayStaticVariableWalk(nodeScopeResolver, stmts, entries.raw(), recording.raw(), staticWalk.raw(), frame.raw(), parentFrame.raw(), parentConstraints.raw(), storage, nodeCallback, scope.raw(), statsEnabled); + } + } + } { bool observingClosures; if (UNEXPECTED(!pt_template_argument_frame_is_observing_closures(frame.raw(), observingClosures))) return zv::Val(); @@ -1635,6 +1653,7 @@ class StatementsHandler zend_long count = 0; zend_long conditionalExpressionsCount = 0; zv::Val constraints; + zv::Val lastWalk; for (;;) { zv::Val siteTypes = staticSiteTypes(staticSites, types.raw()); if (UNEXPECTED(siteTypes.isUndef())) return zv::Val(); @@ -1659,6 +1678,7 @@ class StatementsHandler if (UNEXPECTED(walkRecording.isUndef())) return zv::Val(); zv::Arr replayedScopes = zv::Arr::empty(); zv::Arr carriedOverScopes = zv::Arr::empty(); + zv::Arr walkLog = zv::Arr::empty(); zv::Val suspendedGatherers = pt_node_scope_resolver_suspend_node_gatherers(nodeScopeResolver); if (UNEXPECTED(suspendedGatherers.isUndef())) return zv::Val(); zv::Val pushedScope = zv::Val::copyOf(zv::Ref(OBJ_PROP_NUM(Z_OBJ_P(state.raw()), stateSlots::scope))); @@ -1666,7 +1686,7 @@ class StatementsHandler (void) pt_node_scope_resolver_restore_node_gatherers(nodeScopeResolver, suspendedGatherers.raw()); return zv::Val(); } - (void) observeStaticVariablesFrom(nodeScopeResolver, parentNode, stmts, stmtsTable, frame.raw(), entries, state.raw(), walkStorage.raw(), walkRecording.raw(), context, start, stmtCount, hasLabels, replayedScopes, carriedOverScopes); + (void) observeStaticVariablesFrom(nodeScopeResolver, parentNode, stmts, stmtsTable, frame.raw(), entries, state.raw(), walkStorage.raw(), walkRecording.raw(), context, start, stmtCount, hasLabels, replayedScopes, carriedOverScopes, walkLog); pt_finally([&]() { (void) pt_mutating_scope_pop_expression_result_storage(Z_OBJ_P(pushedScope.raw())); (void) pt_node_scope_resolver_restore_node_gatherers(nodeScopeResolver, suspendedGatherers.raw()); @@ -1680,6 +1700,15 @@ class StatementsHandler constraints = templateArgumentConstraintsCreateEmpty(); if (UNEXPECTED(constraints.isUndef())) return zv::Val(); } + { + zv::Arr walk = zv::Arr::create(5); + walk.push(zv::Val::integer(start)); + walk.push(zv::Val::copyOf(zv::Ref(state.raw()))); + walk.push(zv::Val::copyOf(zv::Ref(walkRecording.raw()))); + walk.push(zv::Val::copyOf(zv::Ref(walkStorage.raw()))); + walk.push(std::move(walkLog)); + lastWalk = zv::Val(std::move(walk)); + } zv::Val walkTypes = collectStaticVariableTypes(bodyScope.raw(), names.raw(), walkRecording.raw(), state.raw(), replayedScopes.raw()); if (UNEXPECTED(walkTypes.isUndef())) return zv::Val(); @@ -1744,18 +1773,82 @@ class StatementsHandler zv::Val siteTypes = staticSiteTypes(staticSites, types.raw()); if (UNEXPECTED(siteTypes.isUndef())) return zv::Val(); - zv::Arr result = zv::Arr::create(3); + // every way out of the loop leaves the last walk made with the types + // and conditional expressions returned + zv::Arr result = zv::Arr::create(4); result.push(std::move(siteTypes)); result.push(std::move(conditionalExpressions)); result.push(std::move(constraints)); + result.push(std::move(lastWalk)); return zv::Val(std::move(result)); } + /* [kind, a, b] of the last static-variable walk's log */ + static zv::Val staticWalkLogEntry(zend_long kind, zend_long a, zend_long b) + { + zv::Arr entry = zv::Arr::create(3); + entry.push(zv::Val::integer(kind)); + entry.push(zv::Val::integer(a)); + entry.push(zv::Val::integer(b)); + return zv::Val(std::move(entry)); + } + + /* Mirrors the private replayStaticVariableWalk() */ + zv::Val replayStaticVariableWalk(zval *nodeScopeResolver, zval *stmts, zval *entries, zval *recording, zval *staticWalk, zval *frame, zval *parentFrame, zval *parentConstraints, zval *storage, zval *nodeCallback, zval *scope, bool statsEnabled) + { + HashTable *walk = Z_ARRVAL_P(staticWalk); + zend_long start = zval_get_long(zend_hash_index_find(walk, 0)); + zval *state = zend_hash_index_find(walk, 1); + zval *walkRecording = zend_hash_index_find(walk, 2); + zval *walkStorage = zend_hash_index_find(walk, 3); + zval *walkLog = zend_hash_index_find(walk, 4); + if (UNEXPECTED(state == NULL || walkRecording == NULL || walkStorage == NULL || walkLog == NULL)) return zv::Val(); + zend_long stmtCount = zend_hash_num_elements(Z_ARRVAL_P(stmts)); + if (statsEnabled) { + if (UNEXPECTED(!templateArgumentStatsIncrement(PT_LC("bodiesWithSites"), 1))) return zv::Val(); + if (UNEXPECTED(!templateArgumentStatsIncrement(PT_LC("staticVariableWalksReplayed"), 1))) return zv::Val(); + } + zval *startEntry = entryAt(entries, start); + if (UNEXPECTED(startEntry == NULL)) return zv::Val(); + if (UNEXPECTED(!pt_node_scope_resolver_replay_recording_range(nodeScopeResolver, recording, 0, zval_get_long(entryOffset(startEntry)), nodeCallback, storage, scope))) return zv::Val(); + if (UNEXPECTED(!pt_expression_result_storage_merge_results(storage, walkStorage))) return zv::Val(); + zv::Val stateHold = zv::Val::copyOf(zv::Ref(state)); + zv::Val logHold = zv::Val::copyOf(zv::Ref(walkLog)); + for (zv::ArrayEntry entry : zv::ArrRef(logHold.raw())) { + HashTable *op = Z_ARRVAL_P(entry.value().deref().raw()); + zend_long kind = zval_get_long(zend_hash_index_find(op, 0)); + zend_long a = zval_get_long(zend_hash_index_find(op, 1)); + zend_long b = zval_get_long(zend_hash_index_find(op, 2)); + if (kind == PT_SH_STATIC_WALK_WALKED) { + if (UNEXPECTED(!pt_node_scope_resolver_replay_recording_range(nodeScopeResolver, walkRecording, a, b, nodeCallback, storage, scope))) return zv::Val(); + continue; + } + zval *entryPair = entryAt(entries, a); + if (UNEXPECTED(entryPair == NULL)) return zv::Val(); + if (kind == PT_SH_STATIC_WALK_CARRIED) { + zval *exitPair = entryAt(entries, a + 1); + if (UNEXPECTED(exitPair == NULL)) return zv::Val(); + if (UNEXPECTED(!pt_node_scope_resolver_replay_recording_range(nodeScopeResolver, recording, zval_get_long(entryOffset(entryPair)), zval_get_long(entryOffset(exitPair)), nodeCallback, storage, scope))) return zv::Val(); + continue; + } + zend_long recorded; + if (UNEXPECTED(!recordingNodeCallbackCount(recording, recorded))) return zv::Val(); + if (UNEXPECTED(!pt_node_scope_resolver_replay_recording_range(nodeScopeResolver, recording, zval_get_long(entryOffset(entryPair)), recorded, nodeCallback, storage, scope))) return zv::Val(); + // the recorded end scope, as the second pass takes it + zval *finalPair = entryAt(entries, stmtCount); + if (UNEXPECTED(finalPair == NULL)) return zv::Val(); + zv::Ref(OBJ_PROP_NUM(Z_OBJ_P(stateHold.raw()), stateSlots::scope)).assign(zv::Val::copyOf(zv::Ref(OBJ_PROP_NUM(Z_OBJ_P(entryState(finalPair)), stateSlots::scope)))); + } + if (UNEXPECTED(!processDeferredByRefClosureBodies(nodeScopeResolver, frame, stateHold.raw(), storage, nodeCallback))) return zv::Val(); + if (UNEXPECTED(!restoreParentFrame(stateHold.raw(), parentFrame, parentConstraints))) return zv::Val(); + return pt_statement_list_walk_state_to_result(stateHold.raw()); + } + /* inferStaticVariableTypes()'s try block: the statements from $start on, * walked or carried over, each walked exit scope and carried-over entry * scope into $replayedScopes, each carried-over entry scope into * $carriedOverScopes; false = pending exception */ - bool observeStaticVariablesFrom(zval *nodeScopeResolver, zval *parentNode, zval *stmts, HashTable *stmtsTable, zval *frame, zval *entries, zval *state, zval *storage, zval *nodeCallback, zval *context, zend_long start, zend_long stmtCount, bool hasLabels, zv::Arr &replayedScopes, zv::Arr &carriedOverScopes) + bool observeStaticVariablesFrom(zval *nodeScopeResolver, zval *parentNode, zval *stmts, HashTable *stmtsTable, zval *frame, zval *entries, zval *state, zval *storage, zval *nodeCallback, zval *context, zend_long start, zend_long stmtCount, bool hasLabels, zv::Arr &replayedScopes, zv::Arr &carriedOverScopes, zv::Arr &walkLog) { for (zend_long i = start; i < stmtCount; i++) { zval *recordedEntryPair = entryAt(entries, i); @@ -1781,7 +1874,9 @@ class StatementsHandler zv::Val carried = withRecordedConstraints(OBJ_PROP_NUM(stateObject, stateSlots::scope), OBJ_PROP_NUM(recordedEntryObject, stateSlots::scope), OBJ_PROP_NUM(Z_OBJ_P(finalState), stateSlots::scope)); if (UNEXPECTED(carried.isUndef())) return false; zv::Ref(OBJ_PROP_NUM(stateObject, stateSlots::scope)).assign(std::move(carried)); - return appendRecordedStatementResults(state, recordedEntry, finalState); + if (UNEXPECTED(!appendRecordedStatementResults(state, recordedEntry, finalState))) return false; + walkLog.push(staticWalkLogEntry(PT_SH_STATIC_WALK_REST, i, 0)); + return true; } } @@ -1795,13 +1890,19 @@ class StatementsHandler if (!reWalk && UNEXPECTED(!frameOwnsSiteInStatement(frame, i, reWalk))) return false; if (!reWalk && UNEXPECTED(!statementMentionsAnyVariable(stmt, differingRoots.raw(), reWalk))) return false; if (reWalk) { + zend_long from; + if (UNEXPECTED(!recordingNodeCallbackCount(nodeCallback, from))) return false; if (UNEXPECTED(!processStatementStep(nodeScopeResolver, parentNode, stmts, i, stmt, state, storage, nodeCallback, context, true))) return false; + zend_long to; + if (UNEXPECTED(!recordingNodeCallbackCount(nodeCallback, to))) return false; + walkLog.push(staticWalkLogEntry(PT_SH_STATIC_WALK_WALKED, from, to)); replayedScopes.push(zv::Ref(OBJ_PROP_NUM(Z_OBJ_P(state), stateSlots::scope))); continue; } // the statement does not read what changed: the variables keep // their types through it + walkLog.push(staticWalkLogEntry(PT_SH_STATIC_WALK_CARRIED, i, 0)); replayedScopes.push(zv::Ref(OBJ_PROP_NUM(Z_OBJ_P(state), stateSlots::scope))); carriedOverScopes.push(zv::Ref(OBJ_PROP_NUM(Z_OBJ_P(state), stateSlots::scope))); zval *recordedExitPair = entryAt(entries, i + 1); @@ -3075,6 +3176,9 @@ PT_MINIT_REGISTRATION(pt_register_statements_handler) { reg::Class cls("PHPStan\\Analyser\\StatementsHandler"); ptdecl::StatementsHandler::declareClass(cls); + cls.privateClassConstantLong("STATIC_WALK_WALKED", PT_SH_STATIC_WALK_WALKED); + cls.privateClassConstantLong("STATIC_WALK_CARRIED", PT_SH_STATIC_WALK_CARRIED); + cls.privateClassConstantLong("STATIC_WALK_REST", PT_SH_STATIC_WALK_REST); cls.privateClassConstantString("MENTIONED_VARIABLES_ATTRIBUTE", "templateArgumentMentionedVariables"); ptdecl::StatementsHandler::declareProperties(cls); diff --git a/turbo-ext/src/StaticVariableInference.cpp b/turbo-ext/src/StaticVariableInference.cpp index 695acf66ba4..7f7d38f6c4a 100644 --- a/turbo-ext/src/StaticVariableInference.cpp +++ b/turbo-ext/src/StaticVariableInference.cpp @@ -29,6 +29,8 @@ namespace { /* the twin's SITES_ATTRIBUTE, a permanent interned string (module startup) */ zend_string *pt_svi_sites_attribute = nullptr; +/* the twin's LAST_FUNCTION_LIKE_ATTRIBUTE, a permanent interned string (module startup) */ +zend_string *pt_svi_last_function_like_attribute = nullptr; pt_method_site pt_svi_get_sub_node_names_site; pt_method_site pt_svi_to_lower_string_site; @@ -325,6 +327,32 @@ zv::Val scanSites(zval *stmts) return zv::Val(std::move(sites)); } +/* Mirrors the private static findLastFunctionLikeStatement(); false = + * pending exception */ +[[nodiscard]] bool findLastFunctionLikeStatement(zval *stmts, zend_long &last) +{ + last = -1; + if (Z_TYPE_P(stmts) != IS_ARRAY) return true; + for (auto stmtEntry : zv::ArrRef(stmts)) { + zend_long index = stmtEntry.stringKeyOrNull() == NULL ? (zend_long) stmtEntry.indexKey() : 0; + zv::Arr stack = zv::Arr::create(0); + stack.push(zv::Val::copyOf(stmtEntry.value().deref())); + for (;;) { + zv::Val node = popNode(stack); + if (node.isUndef()) break; + bool is; + if (UNEXPECTED(!isA(node.raw(), PT_CLASS_FUNCTION_LIKE, is))) return false; + if (!is && UNEXPECTED(!isA(node.raw(), PT_CLASS_CLASS_LIKE_STMT, is))) return false; + if (is) { + last = index; + break; + } + if (UNEXPECTED(!pushSubNodes(node.raw(), stack))) return false; + } + } + return true; +} + } // namespace namespace phpstanturbo { @@ -360,6 +388,24 @@ class StaticVariableInference return sites; } + /* Mirrors hasFunctionLikeFrom(); false = pending exception */ + [[nodiscard]] bool hasFunctionLikeFrom(zval *functionLike, zval *stmts, zend_long index, bool &out) const + { + zv::Val cached = pt_engine_node_get_attribute(Z_OBJ_P(functionLike), ZSTR_VAL(pt_svi_last_function_like_attribute), ZSTR_LEN(pt_svi_last_function_like_attribute)); + if (UNEXPECTED(cached.isUndef())) return false; + zend_long last; + if (cached.isNull()) { + if (UNEXPECTED(!findLastFunctionLikeStatement(stmts, last))) return false; + zval lastZv; + ZVAL_LONG(&lastZv, last); + if (UNEXPECTED(!pt_engine_node_set_attribute(Z_OBJ_P(functionLike), ZSTR_VAL(pt_svi_last_function_like_attribute), ZSTR_LEN(pt_svi_last_function_like_attribute), &lastZv))) return false; + } else { + last = zval_get_long(cached.raw()); + } + out = last >= index; + return true; + } + /* Mirrors isInferred(); false = pending exception */ [[nodiscard]] bool isInferred(zval *scope, zval *var, bool &out) const { @@ -546,6 +592,18 @@ zv::Val pt_static_variable_inference_get_sites(zval *inference, zval *functionLi return pt_type_call(Z_OBJ_P(inference), PT_LC("getsites"), 2, argv); } +bool pt_static_variable_inference_has_function_like_from(zval *inference, zval *functionLike, zval *stmts, zend_long index, bool &out) +{ + if (isNative(inference)) return StaticVariableInference(Z_OBJ_P(inference)).hasFunctionLikeFrom(functionLike, stmts, index, out); + zval indexZv; + ZVAL_LONG(&indexZv, index); + zv::Args argv{functionLike, stmts, &indexZv}; + zv::Val result = pt_type_call(Z_OBJ_P(inference), PT_LC("hasfunctionlikefrom"), 3, argv); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; +} + bool pt_static_variable_inference_is_inferred(zval *inference, zval *scope, zval *var, bool &out) { if (isNative(inference)) return StaticVariableInference(Z_OBJ_P(inference)).isInferred(scope, var, out); @@ -596,11 +654,13 @@ bool pt_static_variable_inference_can_run_user_code(zval *inference, zval *node, PT_MINIT_REGISTRATION(pt_register_static_variable_inference) { pt_svi_sites_attribute = zend_string_init_interned(PT_LC("staticVariableInferenceSites"), 1); + pt_svi_last_function_like_attribute = zend_string_init_interned(PT_LC("staticVariableInferenceLastFunctionLike"), 1); reg::Class cls("PHPStan\\Analyser\\Generics\\StaticVariableInference"); ptdecl::StaticVariableInference::declareClass(cls); ptdecl::StaticVariableInference::declareProperties(cls); cls.privateClassConstantString("SITES_ATTRIBUTE", "staticVariableInferenceSites"); + cls.privateClassConstantString("LAST_FUNCTION_LIKE_ATTRIBUTE", "staticVariableInferenceLastFunctionLike"); /* the real parameter types: the DI container autowires the service by * reflecting the constructor */ @@ -646,6 +706,19 @@ PT_MINIT_REGISTRATION(pt_register_static_variable_inference) PT_RETURN_VAL(StaticVariableInference(Z_OBJ_P(ZEND_THIS)).getSites(functionLike, stmts)); }); + cls.method(sigs::hasFunctionLikeFrom, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *functionLike, *stmts; + zend_long index; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT(functionLike) + Z_PARAM_ARRAY(stmts) + Z_PARAM_LONG(index) + ZEND_PARSE_PARAMETERS_END(); + bool out; + if (UNEXPECTED(!StaticVariableInference(Z_OBJ_P(ZEND_THIS)).hasFunctionLikeFrom(functionLike, stmts, index, out))) RETURN_THROWS(); + RETURN_BOOL(out); + }); + cls.method(sigs::isInferred, [](INTERNAL_FUNCTION_PARAMETERS) { zval *scope, *var; if (!zp::parse(execute_data, scope, var)) RETURN_THROWS(); diff --git a/turbo-ext/src/generated/StatementsHandler.h b/turbo-ext/src/generated/StatementsHandler.h index 45e6a03a74b..804e3ef1442 100644 --- a/turbo-ext/src/generated/StatementsHandler.h +++ b/turbo-ext/src/generated/StatementsHandler.h @@ -100,58 +100,61 @@ inline constexpr char strings[] = "recording\0" /* 1242 */ "PHPStan\\Analyser\\RecordingNodeCallback\0" /* 1252 */ "inferStaticVariableTypes\0" /* 1291 */ - "bodyScope\0" /* 1316 */ - "endState\0" /* 1326 */ - "moreScopes\0" /* 1335 */ - "collectStaticVariableScopes\0" /* 1346 */ - "collectStaticVariableStateScopes\0" /* 1374 */ - "names\0" /* 1407 */ - "scopes\0" /* 1413 */ - "collectStaticVariableTypes\0" /* 1420 */ - "runs\0" /* 1447 */ - "types\0" /* 1452 */ - "walkedConditionalExpressions\0" /* 1458 */ - "enteredTypes\0" /* 1487 */ - "collectStaticVariableConditionalExpressions\0" /* 1500 */ - "isWalkedStaticVariableState\0" /* 1544 */ - "a\0" /* 1572 */ - "b\0" /* 1574 */ - "equalStaticVariableStates\0" /* 1576 */ - "equalStaticVariableConditionalExpressions\0" /* 1602 */ - "PHPStan\\Analyser\\ConditionalExpressionHolder\0" /* 1644 */ - "equalConditionalExpressionHolders\0" /* 1689 */ - "walkTypes\0" /* 1723 */ - "generalize\0" /* 1733 */ - "joinStaticVariableTypes\0" /* 1744 */ - "PHPStan\\Analyser\\ClosureProcessor\0" /* 1768 */ - "getClosureProcessor\0" /* 1802 */ - "frame\0" /* 1822 */ - "processDeferredByRefClosureBodies\0" /* 1828 */ - "statementStartTokenPositions\0" /* 1862 */ - "observeClosureSignatures\0" /* 1891 */ - "recordedEntry\0" /* 1916 */ - "recordedExit\0" /* 1930 */ - "withRecordedConstraints\0" /* 1943 */ - "containsLabels\0" /* 1967 */ - "from\0" /* 1982 */ - "to\0" /* 1987 */ - "appendRecordedStatementResults\0" /* 1990 */ - "PHPStan\\Analyser\\VariableFlow\0" /* 2021 */ - "getVariableMentionFlow\0" /* 2051 */ - "variableNames\0" /* 2074 */ - "statementMentionsAnyVariable\0" /* 2088 */ - "node\0" /* 2117 */ - "mentionsEverything\0" /* 2122 */ - "collectMentionedVariables\0" /* 2141 */ - "statement\0" /* 2167 */ - "getOverridingThrowPoints\0" /* 2177 */ - "defaultExpr\0" /* 2202 */ - "PhpParser\\Node\\Expr\0" /* 2214 */ - "processStmtVarAnnotation\0" /* 2234 */ - "PHPStan\\PhpDoc\\Tag\\VarTag\0" /* 2259 */ - "findSingleVariableLessVarTag\0" /* 2285 */ - "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints\0" /* 2314 */ - "emitVarTagChangedNode"; /* 2368 */ + "staticWalk\0" /* 1316 */ + "frame\0" /* 1327 */ + "parentConstraints\0" /* 1333 */ + "PHPStan\\Analyser\\Generics\\TemplateArgumentConstraints\0" /* 1351 */ + "replayStaticVariableWalk\0" /* 1405 */ + "bodyScope\0" /* 1430 */ + "endState\0" /* 1440 */ + "moreScopes\0" /* 1449 */ + "collectStaticVariableScopes\0" /* 1460 */ + "collectStaticVariableStateScopes\0" /* 1488 */ + "names\0" /* 1521 */ + "scopes\0" /* 1527 */ + "collectStaticVariableTypes\0" /* 1534 */ + "runs\0" /* 1561 */ + "types\0" /* 1566 */ + "walkedConditionalExpressions\0" /* 1572 */ + "enteredTypes\0" /* 1601 */ + "collectStaticVariableConditionalExpressions\0" /* 1614 */ + "isWalkedStaticVariableState\0" /* 1658 */ + "a\0" /* 1686 */ + "b\0" /* 1688 */ + "equalStaticVariableStates\0" /* 1690 */ + "equalStaticVariableConditionalExpressions\0" /* 1716 */ + "PHPStan\\Analyser\\ConditionalExpressionHolder\0" /* 1758 */ + "equalConditionalExpressionHolders\0" /* 1803 */ + "walkTypes\0" /* 1837 */ + "generalize\0" /* 1847 */ + "joinStaticVariableTypes\0" /* 1858 */ + "PHPStan\\Analyser\\ClosureProcessor\0" /* 1882 */ + "getClosureProcessor\0" /* 1916 */ + "processDeferredByRefClosureBodies\0" /* 1936 */ + "statementStartTokenPositions\0" /* 1970 */ + "observeClosureSignatures\0" /* 1999 */ + "recordedEntry\0" /* 2024 */ + "recordedExit\0" /* 2038 */ + "withRecordedConstraints\0" /* 2051 */ + "containsLabels\0" /* 2075 */ + "from\0" /* 2090 */ + "to\0" /* 2095 */ + "appendRecordedStatementResults\0" /* 2098 */ + "PHPStan\\Analyser\\VariableFlow\0" /* 2129 */ + "getVariableMentionFlow\0" /* 2159 */ + "variableNames\0" /* 2182 */ + "statementMentionsAnyVariable\0" /* 2196 */ + "node\0" /* 2225 */ + "mentionsEverything\0" /* 2230 */ + "collectMentionedVariables\0" /* 2249 */ + "statement\0" /* 2275 */ + "getOverridingThrowPoints\0" /* 2285 */ + "defaultExpr\0" /* 2310 */ + "PhpParser\\Node\\Expr\0" /* 2322 */ + "processStmtVarAnnotation\0" /* 2342 */ + "PHPStan\\PhpDoc\\Tag\\VarTag\0" /* 2367 */ + "findSingleVariableLessVarTag\0" /* 2393 */ + "emitVarTagChangedNode"; /* 2422 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 15), /* __construct $fileTypeMapper */ reg::packed(43, 0, 68), /* __construct $templateArgumentObserver */ @@ -229,49 +232,61 @@ inline constexpr reg::PackedArg args[] = { reg::packed(592, 0, 477), /* inferStaticVariableTypes $storage */ reg::packed(600, 0, 608), /* inferStaticVariableTypes $context */ reg::packed(531, MAY_BE_ARRAY), /* inferStaticVariableTypes return */ - reg::packed(1316, 0, 422), /* collectStaticVariableScopes $bodyScope */ + reg::packed(357, 0, 375), /* replayStaticVariableWalk $nodeScopeResolver */ + reg::packed(865, MAY_BE_ARRAY), /* replayStaticVariableWalk $stmts */ + reg::packed(1234, MAY_BE_ARRAY), /* replayStaticVariableWalk $entries */ + reg::packed(1242, 0, 1252), /* replayStaticVariableWalk $recording */ + reg::packed(1316, MAY_BE_ARRAY), /* replayStaticVariableWalk $staticWalk */ + reg::packed(1327, 0, 1186), /* replayStaticVariableWalk $frame */ + reg::packed(1174, MAY_BE_NULL, 1186), /* replayStaticVariableWalk $parentFrame */ + reg::packed(1333, MAY_BE_NULL, 1351), /* replayStaticVariableWalk $parentConstraints */ + reg::packed(592, 0, 477), /* replayStaticVariableWalk $storage */ + reg::packed(518, MAY_BE_CALLABLE), /* replayStaticVariableWalk $nodeCallback */ + reg::packed(416, 0, 422), /* replayStaticVariableWalk $scope */ + reg::packed(531, 0, 871), /* replayStaticVariableWalk return */ + reg::packed(1430, 0, 422), /* collectStaticVariableScopes $bodyScope */ reg::packed(1242, 0, 1252), /* collectStaticVariableScopes $recording */ - reg::packed(1326, 0, 964), /* collectStaticVariableScopes $endState */ - reg::packed(1335, MAY_BE_ARRAY), /* collectStaticVariableScopes $moreScopes */ + reg::packed(1440, 0, 964), /* collectStaticVariableScopes $endState */ + reg::packed(1449, MAY_BE_ARRAY), /* collectStaticVariableScopes $moreScopes */ reg::packed(531, MAY_BE_ARRAY), /* collectStaticVariableScopes return */ - reg::packed(1316, 0, 422), /* collectStaticVariableStateScopes $bodyScope */ + reg::packed(1430, 0, 422), /* collectStaticVariableStateScopes $bodyScope */ reg::packed(1242, 0, 1252), /* collectStaticVariableStateScopes $recording */ - reg::packed(1326, 0, 964), /* collectStaticVariableStateScopes $endState */ - reg::packed(1335, MAY_BE_ARRAY), /* collectStaticVariableStateScopes $moreScopes */ + reg::packed(1440, 0, 964), /* collectStaticVariableStateScopes $endState */ + reg::packed(1449, MAY_BE_ARRAY), /* collectStaticVariableStateScopes $moreScopes */ reg::packed(531, MAY_BE_ARRAY), /* collectStaticVariableStateScopes return */ - reg::packed(1407, MAY_BE_ARRAY), /* collectStaticVariableTypes $names */ - reg::packed(1413, MAY_BE_ARRAY), /* collectStaticVariableTypes $scopes */ + reg::packed(1521, MAY_BE_ARRAY), /* collectStaticVariableTypes $names */ + reg::packed(1527, MAY_BE_ARRAY), /* collectStaticVariableTypes $scopes */ reg::packed(531, MAY_BE_ARRAY), /* collectStaticVariableTypes return */ - reg::packed(1447, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $runs */ - reg::packed(1413, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $scopes */ + reg::packed(1561, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $runs */ + reg::packed(1527, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $scopes */ reg::packed(592, 0, 477), /* collectStaticVariableConditionalExpressions $storage */ - reg::packed(1452, MAY_BE_NULL | MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $types */ - reg::packed(1458, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $walkedConditionalExpressions */ - reg::packed(1487, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $enteredTypes */ + reg::packed(1566, MAY_BE_NULL | MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $types */ + reg::packed(1572, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $walkedConditionalExpressions */ + reg::packed(1601, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions $enteredTypes */ reg::packed(531, MAY_BE_ARRAY), /* collectStaticVariableConditionalExpressions return */ reg::packed(958, MAY_BE_ARRAY), /* isWalkedStaticVariableState $state */ - reg::packed(1452, MAY_BE_ARRAY), /* isWalkedStaticVariableState $types */ + reg::packed(1566, MAY_BE_ARRAY), /* isWalkedStaticVariableState $types */ reg::packed(416, 0, 422), /* isWalkedStaticVariableState $scope */ - reg::packed(1458, MAY_BE_ARRAY), /* isWalkedStaticVariableState $walkedConditionalExpressions */ + reg::packed(1572, MAY_BE_ARRAY), /* isWalkedStaticVariableState $walkedConditionalExpressions */ reg::packed(531, MAY_BE_BOOL), /* isWalkedStaticVariableState return */ - reg::packed(1572, MAY_BE_ARRAY), /* equalStaticVariableStates $a */ - reg::packed(1574, MAY_BE_ARRAY), /* equalStaticVariableStates $b */ + reg::packed(1686, MAY_BE_ARRAY), /* equalStaticVariableStates $a */ + reg::packed(1688, MAY_BE_ARRAY), /* equalStaticVariableStates $b */ reg::packed(531, MAY_BE_BOOL), /* equalStaticVariableStates return */ - reg::packed(1572, MAY_BE_ARRAY), /* equalStaticVariableConditionalExpressions $a */ - reg::packed(1574, MAY_BE_ARRAY), /* equalStaticVariableConditionalExpressions $b */ + reg::packed(1686, MAY_BE_ARRAY), /* equalStaticVariableConditionalExpressions $a */ + reg::packed(1688, MAY_BE_ARRAY), /* equalStaticVariableConditionalExpressions $b */ reg::packed(531, MAY_BE_BOOL), /* equalStaticVariableConditionalExpressions return */ - reg::packed(1572, 0, 1644), /* equalConditionalExpressionHolders $a */ - reg::packed(1574, 0, 1644), /* equalConditionalExpressionHolders $b */ + reg::packed(1686, 0, 1758), /* equalConditionalExpressionHolders $a */ + reg::packed(1688, 0, 1758), /* equalConditionalExpressionHolders $b */ reg::packed(531, MAY_BE_BOOL), /* equalConditionalExpressionHolders return */ - reg::packed(1316, 0, 422), /* joinStaticVariableTypes $bodyScope */ - reg::packed(1407, MAY_BE_ARRAY), /* joinStaticVariableTypes $names */ - reg::packed(1452, MAY_BE_ARRAY), /* joinStaticVariableTypes $types */ - reg::packed(1723, MAY_BE_ARRAY), /* joinStaticVariableTypes $walkTypes */ - reg::packed(1733, MAY_BE_BOOL), /* joinStaticVariableTypes $generalize */ + reg::packed(1430, 0, 422), /* joinStaticVariableTypes $bodyScope */ + reg::packed(1521, MAY_BE_ARRAY), /* joinStaticVariableTypes $names */ + reg::packed(1566, MAY_BE_ARRAY), /* joinStaticVariableTypes $types */ + reg::packed(1837, MAY_BE_ARRAY), /* joinStaticVariableTypes $walkTypes */ + reg::packed(1847, MAY_BE_BOOL), /* joinStaticVariableTypes $generalize */ reg::packed(531, MAY_BE_ARRAY), /* joinStaticVariableTypes return */ - reg::packed(531, 0, 1768), /* getClosureProcessor return */ + reg::packed(531, 0, 1882), /* getClosureProcessor return */ reg::packed(357, 0, 375), /* processDeferredByRefClosureBodies $nodeScopeResolver */ - reg::packed(1822, 0, 1186), /* processDeferredByRefClosureBodies $frame */ + reg::packed(1327, 0, 1186), /* processDeferredByRefClosureBodies $frame */ reg::packed(958, 0, 964), /* processDeferredByRefClosureBodies $state */ reg::packed(592, 0, 477), /* processDeferredByRefClosureBodies $storage */ reg::packed(518, MAY_BE_CALLABLE), /* processDeferredByRefClosureBodies $nodeCallback */ @@ -279,51 +294,51 @@ inline constexpr reg::PackedArg args[] = { reg::packed(357, 0, 375), /* observeClosureSignatures $nodeScopeResolver */ reg::packed(556, 0, 567), /* observeClosureSignatures $parentNode */ reg::packed(865, MAY_BE_ARRAY), /* observeClosureSignatures $stmts */ - reg::packed(1822, 0, 1186), /* observeClosureSignatures $frame */ + reg::packed(1327, 0, 1186), /* observeClosureSignatures $frame */ reg::packed(1234, MAY_BE_ARRAY), /* observeClosureSignatures $entries */ reg::packed(592, 0, 477), /* observeClosureSignatures $storage */ reg::packed(600, 0, 608), /* observeClosureSignatures $context */ - reg::packed(1862, MAY_BE_ARRAY), /* observeClosureSignatures $statementStartTokenPositions */ + reg::packed(1970, MAY_BE_ARRAY), /* observeClosureSignatures $statementStartTokenPositions */ reg::packed(531, 0, 1186), /* observeClosureSignatures return */ reg::packed(416, 0, 422), /* withRecordedConstraints $scope */ - reg::packed(1916, 0, 422), /* withRecordedConstraints $recordedEntry */ - reg::packed(1930, 0, 422), /* withRecordedConstraints $recordedExit */ + reg::packed(2024, 0, 422), /* withRecordedConstraints $recordedEntry */ + reg::packed(2038, 0, 422), /* withRecordedConstraints $recordedExit */ reg::packed(531, 0, 422), /* withRecordedConstraints return */ reg::packed(865, MAY_BE_ARRAY), /* containsLabels $stmts */ reg::packed(531, MAY_BE_BOOL), /* containsLabels return */ reg::packed(958, 0, 964), /* appendRecordedStatementResults $state */ - reg::packed(1982, 0, 964), /* appendRecordedStatementResults $from */ - reg::packed(1987, 0, 964), /* appendRecordedStatementResults $to */ + reg::packed(2090, 0, 964), /* appendRecordedStatementResults $from */ + reg::packed(2095, 0, 964), /* appendRecordedStatementResults $to */ reg::packed(531, MAY_BE_VOID), /* appendRecordedStatementResults return */ reg::packed(933, 0, 938), /* getVariableMentionFlow $stmt */ - reg::packed(531, MAY_BE_NULL, 2021), /* getVariableMentionFlow return */ + reg::packed(531, MAY_BE_NULL, 2129), /* getVariableMentionFlow return */ reg::packed(933, 0, 938), /* statementMentionsAnyVariable $stmt */ - reg::packed(2074, MAY_BE_ARRAY), /* statementMentionsAnyVariable $variableNames */ + reg::packed(2182, MAY_BE_ARRAY), /* statementMentionsAnyVariable $variableNames */ reg::packed(531, MAY_BE_BOOL), /* statementMentionsAnyVariable return */ - reg::packed(2117, 0, 567), /* collectMentionedVariables $node */ - reg::packed(1407, MAY_BE_ARRAY, reg::NoString, true, false), /* collectMentionedVariables $names */ - reg::packed(2122, MAY_BE_BOOL, reg::NoString, true, false), /* collectMentionedVariables $mentionsEverything */ + reg::packed(2225, 0, 567), /* collectMentionedVariables $node */ + reg::packed(1521, MAY_BE_ARRAY, reg::NoString, true, false), /* collectMentionedVariables $names */ + reg::packed(2230, MAY_BE_BOOL, reg::NoString, true, false), /* collectMentionedVariables $mentionsEverything */ reg::packed(531, MAY_BE_VOID), /* collectMentionedVariables return */ - reg::packed(2167, 0, 938), /* getOverridingThrowPoints $statement */ + reg::packed(2275, 0, 938), /* getOverridingThrowPoints $statement */ reg::packed(416, 0, 422), /* getOverridingThrowPoints $scope */ reg::packed(531, MAY_BE_NULL | MAY_BE_ARRAY), /* getOverridingThrowPoints return */ reg::packed(357, 0, 375), /* processStmtVarAnnotation $nodeScopeResolver */ reg::packed(416, 0, 422), /* processStmtVarAnnotation $scope */ reg::packed(592, 0, 477), /* processStmtVarAnnotation $storage */ reg::packed(933, 0, 938), /* processStmtVarAnnotation $stmt */ - reg::packed(2202, MAY_BE_NULL, 2214), /* processStmtVarAnnotation $defaultExpr */ + reg::packed(2310, MAY_BE_NULL, 2322), /* processStmtVarAnnotation $defaultExpr */ reg::packed(518, MAY_BE_CALLABLE), /* processStmtVarAnnotation $nodeCallback */ reg::packed(531, 0, 422), /* processStmtVarAnnotation return */ reg::packed(416, 0, 422), /* findSingleVariableLessVarTag $scope */ reg::packed(933, 0, 938), /* findSingleVariableLessVarTag $stmt */ - reg::packed(531, MAY_BE_NULL, 2259), /* findSingleVariableLessVarTag return */ + reg::packed(531, MAY_BE_NULL, 2367), /* findSingleVariableLessVarTag return */ reg::packed(357, 0, 375), /* emitVarTagChangedNode $nodeScopeResolver */ reg::packed(416, 0, 422), /* emitVarTagChangedNode $scope */ reg::packed(592, 0, 477), /* emitVarTagChangedNode $storage */ reg::packed(933, 0, 938), /* emitVarTagChangedNode $stmt */ - reg::packed(2202, 0, 2214), /* emitVarTagChangedNode $defaultExpr */ + reg::packed(2310, 0, 2322), /* emitVarTagChangedNode $defaultExpr */ reg::packed(518, MAY_BE_CALLABLE), /* emitVarTagChangedNode $nodeCallback */ - reg::packed(531, 0, 2314), /* emitVarTagChangedNode return */ + reg::packed(531, 0, 1351), /* emitVarTagChangedNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -341,28 +356,29 @@ inline constexpr sigtab::Sig processStatementStep = { { 1029 /* processStatement inline constexpr sigtab::Sig readEndStatementExprResult = { { 1084 /* readEndStatementExprResult */, 2, 54, 2, 56, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig processBodyStmtNodesTwoPass = { { 1111 /* processBodyStmtNodesTwoPass */, 7, 57, 7, 64, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig inferStaticVariableTypes = { { 1291 /* inferStaticVariableTypes */, 10, 65, 10, 75, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig collectStaticVariableScopes = { { 1346 /* collectStaticVariableScopes */, 4, 76, 4, 80, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig collectStaticVariableStateScopes = { { 1374 /* collectStaticVariableStateScopes */, 4, 81, 4, 85, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig collectStaticVariableTypes = { { 1420 /* collectStaticVariableTypes */, 2, 86, 2, 88, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig collectStaticVariableConditionalExpressions = { { 1500 /* collectStaticVariableConditionalExpressions */, 6, 89, 6, 95, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isWalkedStaticVariableState = { { 1544 /* isWalkedStaticVariableState */, 4, 96, 4, 100, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig equalStaticVariableStates = { { 1576 /* equalStaticVariableStates */, 2, 101, 2, 103, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig equalStaticVariableConditionalExpressions = { { 1602 /* equalStaticVariableConditionalExpressions */, 2, 104, 2, 106, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig equalConditionalExpressionHolders = { { 1689 /* equalConditionalExpressionHolders */, 2, 107, 2, 109, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig joinStaticVariableTypes = { { 1744 /* joinStaticVariableTypes */, 5, 110, 5, 115, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getClosureProcessor = { { 1802 /* getClosureProcessor */, 0, 116, 0, 116, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processDeferredByRefClosureBodies = { { 1828 /* processDeferredByRefClosureBodies */, 5, 117, 5, 122, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig observeClosureSignatures = { { 1891 /* observeClosureSignatures */, 8, 123, 8, 131, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig withRecordedConstraints = { { 1943 /* withRecordedConstraints */, 3, 132, 3, 135, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig containsLabels = { { 1967 /* containsLabels */, 1, 136, 1, 137, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig appendRecordedStatementResults = { { 1990 /* appendRecordedStatementResults */, 3, 138, 3, 141, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getVariableMentionFlow = { { 2051 /* getVariableMentionFlow */, 1, 142, 1, 143, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig statementMentionsAnyVariable = { { 2088 /* statementMentionsAnyVariable */, 2, 144, 2, 146, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig collectMentionedVariables = { { 2141 /* collectMentionedVariables */, 3, 147, 3, 150, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getOverridingThrowPoints = { { 2177 /* getOverridingThrowPoints */, 2, 151, 2, 153, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processStmtVarAnnotation = { { 2234 /* processStmtVarAnnotation */, 6, 154, 6, 160, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig findSingleVariableLessVarTag = { { 2285 /* findSingleVariableLessVarTag */, 2, 161, 2, 163, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig emitVarTagChangedNode = { { 2368 /* emitVarTagChangedNode */, 6, 164, 6, 170, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig replayStaticVariableWalk = { { 1405 /* replayStaticVariableWalk */, 11, 76, 11, 87, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig collectStaticVariableScopes = { { 1460 /* collectStaticVariableScopes */, 4, 88, 4, 92, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig collectStaticVariableStateScopes = { { 1488 /* collectStaticVariableStateScopes */, 4, 93, 4, 97, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig collectStaticVariableTypes = { { 1534 /* collectStaticVariableTypes */, 2, 98, 2, 100, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig collectStaticVariableConditionalExpressions = { { 1614 /* collectStaticVariableConditionalExpressions */, 6, 101, 6, 107, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isWalkedStaticVariableState = { { 1658 /* isWalkedStaticVariableState */, 4, 108, 4, 112, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig equalStaticVariableStates = { { 1690 /* equalStaticVariableStates */, 2, 113, 2, 115, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig equalStaticVariableConditionalExpressions = { { 1716 /* equalStaticVariableConditionalExpressions */, 2, 116, 2, 118, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig equalConditionalExpressionHolders = { { 1803 /* equalConditionalExpressionHolders */, 2, 119, 2, 121, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig joinStaticVariableTypes = { { 1858 /* joinStaticVariableTypes */, 5, 122, 5, 127, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getClosureProcessor = { { 1916 /* getClosureProcessor */, 0, 128, 0, 128, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processDeferredByRefClosureBodies = { { 1936 /* processDeferredByRefClosureBodies */, 5, 129, 5, 134, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig observeClosureSignatures = { { 1999 /* observeClosureSignatures */, 8, 135, 8, 143, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig withRecordedConstraints = { { 2051 /* withRecordedConstraints */, 3, 144, 3, 147, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig containsLabels = { { 2075 /* containsLabels */, 1, 148, 1, 149, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig appendRecordedStatementResults = { { 2098 /* appendRecordedStatementResults */, 3, 150, 3, 153, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getVariableMentionFlow = { { 2159 /* getVariableMentionFlow */, 1, 154, 1, 155, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig statementMentionsAnyVariable = { { 2196 /* statementMentionsAnyVariable */, 2, 156, 2, 158, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig collectMentionedVariables = { { 2249 /* collectMentionedVariables */, 3, 159, 3, 162, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getOverridingThrowPoints = { { 2285 /* getOverridingThrowPoints */, 2, 163, 2, 165, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processStmtVarAnnotation = { { 2342 /* processStmtVarAnnotation */, 6, 166, 6, 172, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig findSingleVariableLessVarTag = { { 2393 /* findSingleVariableLessVarTag */, 2, 173, 2, 175, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig emitVarTagChangedNode = { { 2422 /* emitVarTagChangedNode */, 6, 176, 6, 182, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::StatementsHandler diff --git a/turbo-ext/src/generated/StaticVariableInference.h b/turbo-ext/src/generated/StaticVariableInference.h index 43b593ce60c..0ec6ce6026c 100644 --- a/turbo-ext/src/generated/StaticVariableInference.h +++ b/turbo-ext/src/generated/StaticVariableInference.h @@ -38,56 +38,65 @@ inline constexpr char strings[] = "stmts\0" /* 105 */ "\0" /* 111 */ "getSites\0" /* 112 */ - "scope\0" /* 121 */ - "PHPStan\\Analyser\\MutatingScope\0" /* 127 */ - "var\0" /* 158 */ - "PhpParser\\Node\\Expr\\Variable\0" /* 162 */ - "isInferred\0" /* 191 */ - "getResolvedTypes\0" /* 202 */ - "getRuns\0" /* 219 */ - "stmt\0" /* 227 */ - "PhpParser\\Node\\Stmt\\Static_\0" /* 232 */ - "getResolvedConditionalExpressions\0" /* 260 */ - "node\0" /* 294 */ - "canRunUserCode\0" /* 299 */ - "siteIds\0" /* 314 */ - "declaresOnlySites\0" /* 322 */ - "scanSites\0" /* 340 */ - "expr\0" /* 350 */ - "PhpParser\\Node\\Expr\0" /* 355 */ - "names\0" /* 375 */ - "collectRootNames\0" /* 381 */ - "hasVarTag"; /* 398 */ + "index\0" /* 121 */ + "hasFunctionLikeFrom\0" /* 127 */ + "scope\0" /* 147 */ + "PHPStan\\Analyser\\MutatingScope\0" /* 153 */ + "var\0" /* 184 */ + "PhpParser\\Node\\Expr\\Variable\0" /* 188 */ + "isInferred\0" /* 217 */ + "getResolvedTypes\0" /* 228 */ + "getRuns\0" /* 245 */ + "stmt\0" /* 253 */ + "PhpParser\\Node\\Stmt\\Static_\0" /* 258 */ + "getResolvedConditionalExpressions\0" /* 286 */ + "node\0" /* 320 */ + "canRunUserCode\0" /* 325 */ + "siteIds\0" /* 340 */ + "declaresOnlySites\0" /* 348 */ + "scanSites\0" /* 366 */ + "findLastFunctionLikeStatement\0" /* 376 */ + "expr\0" /* 406 */ + "PhpParser\\Node\\Expr\0" /* 411 */ + "names\0" /* 431 */ + "collectRootNames\0" /* 437 */ + "hasVarTag"; /* 454 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 19), /* __construct $reflectionProvider */ reg::packed(57, MAY_BE_BOOL), /* __construct $enabled */ reg::packed(77, 0, 90), /* getSites $functionLike */ reg::packed(105, MAY_BE_ARRAY), /* getSites $stmts */ reg::packed(111, MAY_BE_ARRAY), /* getSites return */ - reg::packed(121, 0, 127), /* isInferred $scope */ - reg::packed(158, 0, 162), /* isInferred $var */ + reg::packed(77, 0, 90), /* hasFunctionLikeFrom $functionLike */ + reg::packed(105, MAY_BE_ARRAY), /* hasFunctionLikeFrom $stmts */ + reg::packed(121, MAY_BE_LONG), /* hasFunctionLikeFrom $index */ + reg::packed(111, MAY_BE_BOOL), /* hasFunctionLikeFrom return */ + reg::packed(147, 0, 153), /* isInferred $scope */ + reg::packed(184, 0, 188), /* isInferred $var */ reg::packed(111, MAY_BE_BOOL), /* isInferred return */ - reg::packed(121, 0, 127), /* getResolvedTypes $scope */ - reg::packed(158, 0, 162), /* getResolvedTypes $var */ + reg::packed(147, 0, 153), /* getResolvedTypes $scope */ + reg::packed(184, 0, 188), /* getResolvedTypes $var */ reg::packed(111, MAY_BE_NULL | MAY_BE_ARRAY), /* getResolvedTypes return */ reg::packed(77, 0, 90), /* getRuns $functionLike */ reg::packed(105, MAY_BE_ARRAY), /* getRuns $stmts */ reg::packed(111, MAY_BE_ARRAY), /* getRuns return */ - reg::packed(121, 0, 127), /* getResolvedConditionalExpressions $scope */ - reg::packed(227, 0, 232), /* getResolvedConditionalExpressions $stmt */ + reg::packed(147, 0, 153), /* getResolvedConditionalExpressions $scope */ + reg::packed(253, 0, 258), /* getResolvedConditionalExpressions $stmt */ reg::packed(111, MAY_BE_ARRAY), /* getResolvedConditionalExpressions return */ - reg::packed(294, 0, 90), /* canRunUserCode $node */ - reg::packed(121, 0, 127), /* canRunUserCode $scope */ + reg::packed(320, 0, 90), /* canRunUserCode $node */ + reg::packed(147, 0, 153), /* canRunUserCode $scope */ reg::packed(111, MAY_BE_BOOL), /* canRunUserCode return */ - reg::packed(227, 0, 232), /* declaresOnlySites $stmt */ - reg::packed(314, MAY_BE_ARRAY), /* declaresOnlySites $siteIds */ + reg::packed(253, 0, 258), /* declaresOnlySites $stmt */ + reg::packed(340, MAY_BE_ARRAY), /* declaresOnlySites $siteIds */ reg::packed(111, MAY_BE_BOOL), /* declaresOnlySites return */ reg::packed(105, MAY_BE_ARRAY), /* scanSites $stmts */ reg::packed(111, MAY_BE_ARRAY), /* scanSites return */ - reg::packed(350, 0, 355), /* collectRootNames $expr */ - reg::packed(375, MAY_BE_ARRAY, reg::NoString, true, false), /* collectRootNames $names */ + reg::packed(105, MAY_BE_ARRAY), /* findLastFunctionLikeStatement $stmts */ + reg::packed(111, MAY_BE_LONG), /* findLastFunctionLikeStatement return */ + reg::packed(406, 0, 411), /* collectRootNames $expr */ + reg::packed(431, MAY_BE_ARRAY, reg::NoString, true, false), /* collectRootNames $names */ reg::packed(111, MAY_BE_VOID), /* collectRootNames return */ - reg::packed(227, 0, 232), /* hasVarTag $stmt */ + reg::packed(253, 0, 258), /* hasVarTag $stmt */ reg::packed(111, MAY_BE_BOOL), /* hasVarTag return */ }; using Sig = reg::Sig; @@ -97,15 +106,17 @@ using Sig = reg::Sig; namespace sig { inline constexpr sigtab::Sig __construct = { { 65 /* __construct */, 2, 0, 2, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getSites = { { 112 /* getSites */, 2, 2, 2, 4, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isInferred = { { 191 /* isInferred */, 2, 5, 2, 7, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getResolvedTypes = { { 202 /* getResolvedTypes */, 2, 8, 2, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getRuns = { { 219 /* getRuns */, 2, 11, 2, 13, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getResolvedConditionalExpressions = { { 260 /* getResolvedConditionalExpressions */, 2, 14, 2, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig canRunUserCode = { { 299 /* canRunUserCode */, 2, 17, 2, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig declaresOnlySites = { { 322 /* declaresOnlySites */, 2, 20, 2, 22, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig scanSites = { { 340 /* scanSites */, 1, 23, 1, 24, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig collectRootNames = { { 381 /* collectRootNames */, 2, 25, 2, 27, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig hasVarTag = { { 398 /* hasVarTag */, 1, 28, 1, 29, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig hasFunctionLikeFrom = { { 127 /* hasFunctionLikeFrom */, 3, 5, 3, 8, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isInferred = { { 217 /* isInferred */, 2, 9, 2, 11, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getResolvedTypes = { { 228 /* getResolvedTypes */, 2, 12, 2, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getRuns = { { 245 /* getRuns */, 2, 15, 2, 17, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getResolvedConditionalExpressions = { { 286 /* getResolvedConditionalExpressions */, 2, 18, 2, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig canRunUserCode = { { 325 /* canRunUserCode */, 2, 21, 2, 23, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig declaresOnlySites = { { 348 /* declaresOnlySites */, 2, 24, 2, 26, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig scanSites = { { 366 /* scanSites */, 1, 27, 1, 28, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig findLastFunctionLikeStatement = { { 376 /* findLastFunctionLikeStatement */, 1, 29, 1, 30, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig collectRootNames = { { 437 /* collectRootNames */, 2, 31, 2, 33, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig hasVarTag = { { 454 /* hasVarTag */, 1, 34, 1, 35, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::StaticVariableInference diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 01a4d3f63a2..a9928e8412f 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -2025,6 +2025,7 @@ zv::Val pt_template_argument_frame_get_static_variable_conditional_expressions(z extern zend_class_entry *pt_ce_static_variable_inference; zv::Val pt_static_variable_inference_get_sites(zval *inference, zval *functionLike, zval *stmts); [[nodiscard]] bool pt_static_variable_inference_is_inferred(zval *inference, zval *scope, zval *var, bool &out); +[[nodiscard]] bool pt_static_variable_inference_has_function_like_from(zval *inference, zval *functionLike, zval *stmts, zend_long index, bool &out); zv::Val pt_static_variable_inference_get_resolved_types(zval *inference, zval *scope, zval *var); zv::Val pt_static_variable_inference_get_runs(zval *inference, zval *functionLike, zval *stmts); zv::Val pt_static_variable_inference_get_resolved_conditional_expressions(zval *inference, zval *scope, zval *stmt); From 38d90eab972b8eccf8b5f449b62ed33e4e197d1a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 12:36:14 +0200 Subject: [PATCH 5/9] Run the benchmark on PHP 8.5 only again Reverts the PHP 7.4 leg of the benchmark (#6121). PHP 7.4 and 8.5 run the same code paths again since the fibers were replaced by NodeCallbackScope, so the downgraded run measured nothing the 8.5 one does not. The benchmark is back to one Baseline and one Test job on PHP 8.5 comparing against tests/bench/storage/baseline.xml, with the PHPBench attributes. The container cache change of #6121 stays, and so do the `// lint` comments on the data files, which RequiredPhpVersionCommentTest asks of them. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- .github/workflows/bench.yml | 83 +-- Makefile | 1 + tests/bench/BenchCase.php | 48 -- tests/bench/RegressionBench.php | 33 +- tests/bench/storage/baseline-7.4.xml | 690 ------------------ .../{baseline-8.5.xml => baseline.xml} | 0 6 files changed, 21 insertions(+), 834 deletions(-) delete mode 100644 tests/bench/storage/baseline-7.4.xml rename tests/bench/storage/{baseline-8.5.xml => baseline.xml} (100%) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 7d770b8e505..2345376754c 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -23,17 +23,10 @@ permissions: jobs: baseline: - name: "Baseline (PHP ${{ matrix.php-version }})" + name: "Baseline" runs-on: ubuntu-latest timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - php-version: - - "7.4" - - "8.5" - steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -47,65 +40,31 @@ jobs: uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.5" tools: pecl extensions: ds,mbstring ini-file: development ini-values: memory_limit=-1 - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.4' - run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 doctrine/instantiator:^1.0 --update-with-dependencies --ignore-platform-reqs" - - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 - - # tests/composer.json pins the Composer platform to PHP 8.2, which would make - # Composer resolve PHPBench's Symfony dependencies to versions that cannot - # even be parsed on PHP 7.4. Unsetting it resolves against the real PHP version. - - name: "Downgrade PHPBench" - if: matrix.php-version == '7.4' - run: | - composer config --unset platform.php --working-dir=tests - composer require --dev phpbench/phpbench:1.2.14 phpunit/phpunit:^9.6 brianium/paratest:^6.5 --update-with-all-dependencies --working-dir=tests - - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 - if: matrix.php-version != '7.4' with: working-directory: "tests/" - - uses: ./.github/actions/downgrade-code - with: - php-version: "${{ matrix.php-version }}" - - name: "Run phpbench baseline" - run: "tests/vendor/bin/phpbench run --dump-file=tests/bench/storage/baseline-${{ matrix.php-version }}.xml --ansi" + run: "tests/vendor/bin/phpbench run --dump-file=tests/bench/storage/baseline.xml --ansi" - # Before committing an artifact as the new baseline, compare the - # values (md5, file_rw) in it with the ones in the currently committed baseline. - # GitHub runners differ in speed by up to 2x, and a baseline recorded on a fast - # runner makes every later run look like a regression. - name: "Upload baseline artifact" uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: phpbench-baseline-${{ matrix.php-version }} - path: tests/bench/storage/baseline-${{ matrix.php-version }}.xml + name: phpbench-baseline + path: tests/bench/storage/baseline.xml test: - name: "Test (PHP ${{ matrix.php-version }})" + name: "Test" runs-on: ubuntu-latest timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - include: - # phpbench 1.2.14 is the last version supporting PHP 7.4 - # and it does not have the aggregate-preview report 'my-report' extends - - php-version: "7.4" - report: "aggregate" - - php-version: "8.5" - report: "my-report" - steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 @@ -119,42 +78,16 @@ jobs: uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 with: coverage: "none" - php-version: "${{ matrix.php-version }}" + php-version: "8.5" tools: pecl extensions: ds,mbstring ini-file: development ini-values: memory_limit=-1 - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.4' - run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 doctrine/instantiator:^1.0 --update-with-dependencies --ignore-platform-reqs" - - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 - - # tests/composer.json pins the Composer platform to PHP 8.2, which would make - # Composer resolve PHPBench's Symfony dependencies to versions that cannot - # even be parsed on PHP 7.4. Unsetting it resolves against the real PHP version. - - name: "Downgrade PHPBench" - if: matrix.php-version == '7.4' - run: | - composer config --unset platform.php --working-dir=tests - composer require --dev phpbench/phpbench:1.2.14 phpunit/phpunit:^9.6 brianium/paratest:^6.5 --update-with-all-dependencies --working-dir=tests - - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 - if: matrix.php-version != '7.4' with: working-directory: "tests/" - - uses: ./.github/actions/downgrade-code - with: - php-version: "${{ matrix.php-version }}" - - # PHPBench does not fail when the baseline file is missing, it just runs - # with an empty baseline and every assertion passes - - name: "Check that the baseline exists" - run: | - test -f tests/bench/storage/baseline-${{ matrix.php-version }}.xml \ - || (echo "Missing tests/bench/storage/baseline-${{ matrix.php-version }}.xml, download it from the phpbench-baseline-${{ matrix.php-version }} artifact of the Baseline job and commit it." && exit 1) - - name: "Run phpbench test" - run: "tests/vendor/bin/phpbench run --file=tests/bench/storage/baseline-${{ matrix.php-version }}.xml --report=${{ matrix.report }} --ansi" + run: "tests/vendor/bin/phpbench run --file=tests/bench/storage/baseline.xml --report=my-report --ansi" diff --git a/Makefile b/Makefile index a49c12e6af3..ea78ec2e2cb 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ lint: XDEBUG_MODE=off php vendor/bin/parallel-lint --colors \ --exclude tests/PHPStan/Analyser/data \ --exclude tests/bench/data \ + --exclude tests/bench/RegressionBench.php \ --exclude tests/PHPStan/Analyser/nsrt \ --exclude tests/PHPStan/Rules/Methods/data \ --exclude tests/PHPStan/Rules/Functions/data \ diff --git a/tests/bench/BenchCase.php b/tests/bench/BenchCase.php index 755b3f3532d..779718d72a8 100644 --- a/tests/bench/BenchCase.php +++ b/tests/bench/BenchCase.php @@ -2,20 +2,10 @@ namespace PHPStan\Benchmark; -use LogicException; use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; use PHPStan\Testing\PHPStanTestCaseTrait; -use function fclose; -use function fgets; -use function fopen; -use function preg_match; -use function sprintf; -use function str_contains; -use function str_starts_with; -use function version_compare; -use const PHP_VERSION; abstract class BenchCase { @@ -29,44 +19,6 @@ public static function getAdditionalConfigFiles(): array ]; } - /** - * Data files that cannot be parsed on the current PHP version are not worth - * benchmarking - the analysis stops at the syntax error. - * - * Copy of TypeInferenceTestCase::isFileLintSkipped(), originally from - * https://github.com/php-parallel-lint/PHP-Parallel-Lint/blob/0c2706086ac36dce31967cb36062ff8915fe03f7/bin/skip-linting.php - * - * Copyright (c) 2012, Jakub Onderka - */ - protected static function isFileLintSkipped(string $file): bool - { - $f = @fopen($file, 'r'); - if ($f !== false) { - $firstLine = fgets($f); - if ($firstLine === false) { - return false; - } - - // ignore shebang line - if (str_starts_with($firstLine, '#!')) { - $firstLine = fgets($f); - if ($firstLine === false) { - return false; - } - } - - @fclose($f); - - if (preg_match('~ diff --git a/tests/bench/RegressionBench.php b/tests/bench/RegressionBench.php index a8929c0bcbe..bef98a0789f 100644 --- a/tests/bench/RegressionBench.php +++ b/tests/bench/RegressionBench.php @@ -2,31 +2,26 @@ namespace PHPStan\Benchmark; +use PhpBench\Attributes as Bench; use Symfony\Component\Finder\Finder; -/** - * PHPBench annotations are used instead of attributes so that the benchmark - * also runs on PHP 7.4 with the downgraded source code, where attributes - * are just comments. - * - * The assertion expression has to stay on a single line - an annotation value - * cannot span multiple lines. Annotation names must not be mentioned anywhere - * else in this docblock either, the annotation reader tries to parse them. - * - * @Revs(1) - * @Iterations(5) - * @Warmup(1) - * @RetryThreshold(10.0) - * @Assert("(mode(baseline.time.avg) < 100 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 50%) or (mode(baseline.time.avg) >= 100 milliseconds and mode(baseline.time.avg) < 500 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 25%) or (mode(baseline.time.avg) >= 500 milliseconds and mode(baseline.time.avg) < 2000 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 20%) or (mode(baseline.time.avg) >= 2000 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 10%)") - */ +#[Bench\Revs(revs: 1)] +#[Bench\Iterations(iterations: 5)] +#[Bench\Warmup(revs: 1)] +#[Bench\RetryThreshold(retryThreshold: 10.0)] +#[Bench\Assert(expression: ' + (mode(baseline.time.avg) < 100 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 50%) + or (mode(baseline.time.avg) >= 100 milliseconds and mode(baseline.time.avg) < 500 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 25%) + or (mode(baseline.time.avg) >= 500 milliseconds and mode(baseline.time.avg) < 2000 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 20%) + or (mode(baseline.time.avg) >= 2000 milliseconds and mode(variant.time.avg) < mode(baseline.time.avg) +/- 10%) +')] class RegressionBench extends BenchCase { /** - * @ParamProviders({"provideFiles"}) - * * @param array{string} $params */ + #[Bench\ParamProviders(['provideFiles'])] public function benchRunAnalyse(array $params): void { $this->runAnalyse($params[0]); @@ -47,10 +42,6 @@ private static function findTestDataFilesFromDirectory(string $directory): array $finder->sortByName(true); $files = []; foreach ($finder->files()->name('*.php')->in($directory) as $fileInfo) { - if (self::isFileLintSkipped($fileInfo->getPathname())) { - continue; - } - $files[$fileInfo->getBasename()] = [$fileInfo->getPathname()]; } diff --git a/tests/bench/storage/baseline-7.4.xml b/tests/bench/storage/baseline-7.4.xml deleted file mode 100644 index 4c733cfe030..00000000000 --- a/tests/bench/storage/baseline-7.4.xml +++ /dev/null @@ -1,690 +0,0 @@ - - - - - - Linux - runnervmvrwv9 - 6.17.0-1020-azure - #20~24.04.1-Ubuntu SMP Fri Jun 19 20:09:14 UTC 2026 - x86_64 - - - - 7.4.33 - /etc/php/7.4/cli/php.ini - Core, date, libxml, openssl, pcre, zlib, filter, hash, pcntl, Reflection, SPL, session, standard, sodium, mysqlnd, PDO, xml, amqp, apcu, ast, bcmath, bz2, calendar, ctype, curl, dba, dom, enchant, mbstring, FFI, fileinfo, ftp, gd, gettext, gmp, iconv, igbinary, imagick, imap, intl, json, ldap, exif, memcache, mongodb, msgpack, mysqli, odbc, pdo_dblib, PDO_Firebird, pdo_mysql, PDO_ODBC, pdo_pgsql, pdo_sqlite, pdo_sqlsrv, pgsql, Phar, posix, pspell, readline, shmop, SimpleXML, snmp, soap, sockets, sqlite3, sqlsrv, sysvmsg, sysvsem, sysvshm, tidy, tokenizer, xmlreader, xmlrpc, xmlwriter, xsl, yaml, zip, zmq, memcached, redis, ds, Zend OPcache - - - 1 - - - - 1.572265625 - 0.69873046875 - 0.2646484375 - - - git - 2.2.x - 81e06a5839991d8d3f3b17c97f6e92eb0a6f5906 - - - 0.0090599060058594 - 0.1530647277832 - 1.8279552459717 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/bench/storage/baseline-8.5.xml b/tests/bench/storage/baseline.xml similarity index 100% rename from tests/bench/storage/baseline-8.5.xml rename to tests/bench/storage/baseline.xml From b2bd56c268a948e9b29ae08500a409832cab5f8d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 13:20:51 +0200 Subject: [PATCH 6/9] Skip keys-superset checks of arrays differing in constant scalar values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since unions of constant arrays with many keys stopped degrading (e606ca20e3), reduceArrays() asks isKeysSupersetOf() in both directions of every pair of arrays sharing a key - a union of n same-shaped records costs n² checks of their value types. The bench's bug-12671 made 654K of them for a few dozen unions of state tuples, none of which merged. Each array's keys holding a single constant scalar are collected once per call. Two such values that differ are disjoint, and isKeysSupersetOf() of arrays without real unsealed extras lets at most one key's values be unrelated (only when both have the same number of keys, two or more) - a pair differing in more is skipped without asking. Merged arrays drop their collected values. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- src/Type/TypeCombinator.php | 88 ++++++++++++++++++++ turbo-ext/src/TypeCombinator.cpp | 100 ++++++++++++++++++++++- turbo-ext/src/generated/TypeCombinator.h | 76 +++++++++-------- 3 files changed, 230 insertions(+), 34 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 6508e03ce58..9921d0dd991 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1385,6 +1385,69 @@ public static function countConstantArrayValueTypes(array $types): int return $constantArrayValuesCount; } + /** + * The value at each key of the array that is a single constant scalar, null + * when the array has real unsealed extras - its keys-supersets are then not + * decided by the known keys alone. + * + * @return array|null + */ + private static function getConstantScalarValuesByKey(ConstantArrayType $array): ?array + { + $unsealed = $array->getUnsealedTypes(); + if ($unsealed !== null && !($unsealed[0] instanceof NeverType && $unsealed[0]->isExplicit())) { + return null; + } + + $values = []; + $valueTypes = $array->getValueTypes(); + foreach ($array->getKeyTypes() as $i => $keyType) { + if (!$valueTypes[$i]->isConstantScalarValue()->yes()) { + continue; + } + $constantScalarValues = $valueTypes[$i]->getConstantScalarValues(); + if (count($constantScalarValues) !== 1) { + continue; + } + $values[$keyType->getValue()] = $constantScalarValues[0]; + } + + return $values; + } + + /** + * Whether two arrays differ in enough keys holding different single constant + * scalars that neither can be a keys-superset of the other: each such key + * pairs two disjoint values, and ConstantArrayType::isKeysSupersetOf() lets + * at most one key's values be unrelated - only when both arrays have the + * same number of keys, two or more. + * + * @param array|null $a + * @param array|null $b + */ + private static function haveDisjointConstantScalarValues(?array $a, ?array $b, bool $oneUnrelatedKeyAllowed): bool + { + if ($a === null || $b === null) { + return false; + } + if (count($a) > count($b)) { + [$a, $b] = [$b, $a]; + } + + $differing = 0; + foreach ($a as $key => $value) { + if (!array_key_exists($key, $b) || $b[$key] === $value) { + continue; + } + $differing++; + if ($differing >= 2 || !$oneUnrelatedKeyAllowed) { + return true; + } + } + + return false; + } + /** * @param list $constantArrays * @return list @@ -1431,6 +1494,15 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged } } + // the single constant scalar value at each key of the arrays without + // real unsealed extras (whose supersets the key/value shape decides) + $constantScalarValues = []; + if ($preserveTaggedUnions) { + foreach ($arraysToProcess as $i => $arrayToProcess) { + $constantScalarValues[$i] = self::getConstantScalarValuesByKey($arrayToProcess); + } + } + $eligibleCombinations = []; foreach ($arraysToProcessPerKey as $arrays) { @@ -1468,17 +1540,32 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged $unionValueType = self::union($iValueType, $jValueType); if (!$unionValueType instanceof UnionType) { $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); + $constantScalarValues[$j] = null; unset($arraysToProcess[$i]); continue 2; } } + if ( + $preserveTaggedUnions + && self::haveDisjointConstantScalarValues( + $constantScalarValues[$i] ?? null, + $constantScalarValues[$j] ?? null, + count($arraysToProcess[$i]->getKeyTypes()) === count($arraysToProcess[$j]->getKeyTypes()) + && count($arraysToProcess[$i]->getKeyTypes()) >= 2, + ) + ) { + // neither array is a keys-superset of the other + continue; + } + if ( $preserveTaggedUnions && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes()) && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i]) ) { $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); + $constantScalarValues[$j] = null; unset($arraysToProcess[$i]); continue 2; } @@ -1489,6 +1576,7 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j]) ) { $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]); + $constantScalarValues[$i] = null; unset($arraysToProcess[$j]); continue 1; } diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index 6b0a263ec42..f744bbbedb2 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -2449,6 +2449,77 @@ class TypeCombinator return callBool(a, PT_LC("iskeyssupersetof"), 1, b); } + /* Mirrors getConstantScalarValuesByKey(): key => the single constant + * scalar value there, IS_NULL when the array has real unsealed extras; + * UNDEF = pending exception */ + static zv::Val constantScalarValuesByKey(zval *array) + { + zv::Val unsealed = call(array, PT_LC("getunsealedtypes")); + PT_FAIL_IF_UNDEF(unsealed); + if (!isNull(unsealed)) { + zval *unsealedKey = indexOf(unsealed.raw(), 0); + if (UNEXPECTED(unsealedKey == NULL)) { + zend_throw_error(NULL, "Undefined array key 0"); + return zv::Val(); + } + int explicitNever = isExplicitNever(unsealedKey); + PT_FAIL_IF_NEG(explicitNever); + if (!explicitNever) return zv::Val::null(); + } + + zv::Arr values = zv::Arr::create(0); + zv::Val valueTypes = call(array, PT_LC("getvaluetypes")); + PT_FAIL_IF_UNDEF(valueTypes); + zv::Val keyTypes = call(array, PT_LC("getkeytypes")); + PT_FAIL_IF_UNDEF(keyTypes); + for (zv::ArrayEntry keyEntry : zv::ArrRef(keyTypes.raw())) { + zval *valueType = zend_hash_index_find(Z_ARRVAL_P(valueTypes.raw()), keyEntry.indexKey()); + if (UNEXPECTED(valueType == NULL)) { + zend_throw_error(NULL, "Undefined array key " ZEND_ULONG_FMT, keyEntry.indexKey()); + return zv::Val(); + } + zend_long isConstantScalar = callTrinary(valueType, PT_LC("isconstantscalarvalue")); + PT_FAIL_IF_NEG(isConstantScalar); + if (isConstantScalar != PT_TRI_YES) continue; + zv::Val constantScalarValues = call(valueType, PT_LC("getconstantscalarvalues")); + PT_FAIL_IF_UNDEF(constantScalarValues); + if (countOf(constantScalarValues.raw()) != 1) continue; + zv::Val keyValue = call(keyEntry.value().raw(), PT_LC("getvalue")); + PT_FAIL_IF_UNDEF(keyValue); + zval value; + ZVAL_COPY(&value, indexOf(constantScalarValues.raw(), 0)); + if (UNEXPECTED(!symtableSet(values.table(), keyValue.raw(), &value))) return zv::Val(); + } + return zv::Val(std::move(values)); + } + + /* Mirrors haveDisjointConstantScalarValues() */ + static bool haveDisjointConstantScalarValues(zval *a, zval *b, bool oneUnrelatedKeyAllowed) + { + if (a == NULL || b == NULL || Z_TYPE_P(a) != IS_ARRAY || Z_TYPE_P(b) != IS_ARRAY) return false; + if (zend_hash_num_elements(Z_ARRVAL_P(a)) > zend_hash_num_elements(Z_ARRVAL_P(b))) std::swap(a, b); + int differing = 0; + zend_ulong index; + zend_string *key; + zval *value; + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(a), index, key, value) { + zval *other = key != NULL ? zend_hash_find(Z_ARRVAL_P(b), key) : zend_hash_index_find(Z_ARRVAL_P(b), index); + if (other == NULL || zend_is_identical(other, value)) continue; + differing++; + if (differing >= 2 || !oneUnrelatedKeyAllowed) return true; + } ZEND_HASH_FOREACH_END(); + return false; + } + + /* $constantScalarValues[$index] = null */ + static void forgetConstantScalarValues(zv::Arr &constantScalarValues, zend_ulong index) + { + zval null; + ZVAL_NULL(&null); + constantScalarValues.separate(); + zend_hash_index_update(constantScalarValues.table(), index, &null); + } + /* private static reduceArrays(list $constantArrays, bool $preserveTaggedUnions): list */ static zv::Val reduceArrays(zval *constantArrays, bool preserveTaggedUnions) { @@ -2512,6 +2583,19 @@ class TypeCombinator } } + /* the single constant scalar value at each key of the arrays without + * real unsealed extras */ + zv::Arr constantScalarValues = zv::Arr::create(0); + if (preserveTaggedUnions) { + for (zv::ArrayEntry entry : zv::ArrRef(arraysToProcess.raw())) { + zv::Val values = constantScalarValuesByKey(entry.value().raw()); + PT_FAIL_IF_UNDEF(values); + zval v = values.take(); + constantScalarValues.separate(); + zend_hash_index_update(constantScalarValues.table(), entry.indexKey(), &v); + } + } + zv::Arr eligibleCombinations = zv::Arr::create(0); for (zv::ArrayEntry entry : zv::ArrRef(arraysToProcessPerKey.raw())) { HashTable *arrays = Z_ARRVAL_P(entry.value().raw()); @@ -2571,27 +2655,39 @@ class TypeCombinator PT_FAIL_IF_UNDEF(unionValueType); if (!isInstance(unionValueType.raw(), pt_ce_union_type)) { if (UNEXPECTED(!mergeInto(arraysToProcess, j, i))) return zv::Val(); + forgetConstantScalarValues(constantScalarValues, j); break; } } } + zend_long jKeysCount = keyTypesCount(arrayAt(arraysToProcess, j)); + PT_FAIL_IF_NEG(jKeysCount); + if (preserveTaggedUnions + && haveDisjointConstantScalarValues( + zend_hash_index_find(constantScalarValues.table(), i), + zend_hash_index_find(constantScalarValues.table(), j), + iKeysCount == jKeysCount && iKeysCount >= 2)) { + /* neither array is a keys-superset of the other */ + continue; + } + if (preserveTaggedUnions && overlappingKeysCount == iKeysCount) { int superset = isKeysSupersetOf(arrayAt(arraysToProcess, j), arrayAt(arraysToProcess, i)); PT_FAIL_IF_NEG(superset); if (superset) { if (UNEXPECTED(!mergeInto(arraysToProcess, j, i))) return zv::Val(); + forgetConstantScalarValues(constantScalarValues, j); break; } } - zend_long jKeysCount = keyTypesCount(arrayAt(arraysToProcess, j)); - PT_FAIL_IF_NEG(jKeysCount); if (preserveTaggedUnions && overlappingKeysCount == jKeysCount) { int superset = isKeysSupersetOf(arrayAt(arraysToProcess, i), arrayAt(arraysToProcess, j)); PT_FAIL_IF_NEG(superset); if (superset) { if (UNEXPECTED(!mergeInto(arraysToProcess, i, j))) return zv::Val(); + forgetConstantScalarValues(constantScalarValues, i); continue; } } diff --git a/turbo-ext/src/generated/TypeCombinator.h b/turbo-ext/src/generated/TypeCombinator.h index 056b917e82d..613d52592e3 100644 --- a/turbo-ext/src/generated/TypeCombinator.h +++ b/turbo-ext/src/generated/TypeCombinator.h @@ -55,20 +55,24 @@ inline constexpr char strings[] = "processArrayTypes\0" /* 472 */ "optimizeConstantArrays\0" /* 490 */ "countConstantArrayValueTypes\0" /* 513 */ - "constantArrays\0" /* 542 */ - "preserveTaggedUnions\0" /* 557 */ - "reduceArrays\0" /* 578 */ - "PHPStan\\Type\\UnionType\0" /* 591 */ - "intersectFiniteUnions\0" /* 614 */ - "finiteUnionMembers\0" /* 636 */ - "intersect\0" /* 655 */ - "doIntersect\0" /* 665 */ - "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 677 */ - "intersectDefiniteConstantArrays\0" /* 717 */ - "PHPStan\\Type\\IntersectionType\0" /* 749 */ - "mergeIntersectionsForUnion\0" /* 779 */ - "removeFalsey\0" /* 806 */ - "removeTruthy"; /* 819 */ + "array\0" /* 542 */ + "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 548 */ + "getConstantScalarValuesByKey\0" /* 588 */ + "oneUnrelatedKeyAllowed\0" /* 617 */ + "haveDisjointConstantScalarValues\0" /* 640 */ + "constantArrays\0" /* 673 */ + "preserveTaggedUnions\0" /* 688 */ + "reduceArrays\0" /* 709 */ + "PHPStan\\Type\\UnionType\0" /* 722 */ + "intersectFiniteUnions\0" /* 745 */ + "finiteUnionMembers\0" /* 767 */ + "intersect\0" /* 786 */ + "doIntersect\0" /* 796 */ + "intersectDefiniteConstantArrays\0" /* 808 */ + "PHPStan\\Type\\IntersectionType\0" /* 840 */ + "mergeIntersectionsForUnion\0" /* 870 */ + "removeFalsey\0" /* 897 */ + "removeTruthy"; /* 910 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_VOID), /* clearCache return */ reg::packed(12, 0, 17), /* addNull $type */ @@ -118,23 +122,29 @@ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_ARRAY), /* optimizeConstantArrays return */ reg::packed(126, MAY_BE_ARRAY), /* countConstantArrayValueTypes $types */ reg::packed(0, MAY_BE_LONG), /* countConstantArrayValueTypes return */ - reg::packed(542, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ - reg::packed(557, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */ + reg::packed(542, 0, 548), /* getConstantScalarValuesByKey $array */ + reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* getConstantScalarValuesByKey return */ + reg::packed(164, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $a */ + reg::packed(166, MAY_BE_NULL | MAY_BE_ARRAY), /* haveDisjointConstantScalarValues $b */ + reg::packed(617, MAY_BE_BOOL), /* haveDisjointConstantScalarValues $oneUnrelatedKeyAllowed */ + reg::packed(0, MAY_BE_BOOL), /* haveDisjointConstantScalarValues return */ + reg::packed(673, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ + reg::packed(688, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */ reg::packed(0, MAY_BE_ARRAY), /* reduceArrays return */ - reg::packed(164, 0, 591), /* intersectFiniteUnions $a */ - reg::packed(166, 0, 591), /* intersectFiniteUnions $b */ + reg::packed(164, 0, 722), /* intersectFiniteUnions $a */ + reg::packed(166, 0, 722), /* intersectFiniteUnions $b */ reg::packed(0, MAY_BE_NULL, 17), /* intersectFiniteUnions return */ - reg::packed(132, 0, 591), /* finiteUnionMembers $union */ + reg::packed(132, 0, 722), /* finiteUnionMembers $union */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* finiteUnionMembers return */ reg::packed(126, 0, 17, false, true), /* intersect $types */ reg::packed(0, 0, 17), /* intersect return */ reg::packed(126, 0, 17, false, true), /* doIntersect $types */ reg::packed(0, 0, 17), /* doIntersect return */ - reg::packed(164, 0, 677), /* intersectDefiniteConstantArrays $a */ - reg::packed(166, 0, 677), /* intersectDefiniteConstantArrays $b */ + reg::packed(164, 0, 548), /* intersectDefiniteConstantArrays $a */ + reg::packed(166, 0, 548), /* intersectDefiniteConstantArrays $b */ reg::packed(0, 0, 17), /* intersectDefiniteConstantArrays return */ - reg::packed(164, 0, 749), /* mergeIntersectionsForUnion $a */ - reg::packed(166, 0, 749), /* mergeIntersectionsForUnion $b */ + reg::packed(164, 0, 840), /* mergeIntersectionsForUnion $a */ + reg::packed(166, 0, 840), /* mergeIntersectionsForUnion $b */ reg::packed(0, MAY_BE_NULL, 17), /* mergeIntersectionsForUnion return */ reg::packed(12, 0, 17), /* removeFalsey $type */ reg::packed(0, 0, 17), /* removeFalsey return */ @@ -167,15 +177,17 @@ inline constexpr sigtab::Sig findEmptyArrayNextToOversizedArray = { { 437 /* fin inline constexpr sigtab::Sig processArrayTypes = { { 472 /* processArrayTypes */, 1, 42, 1, 43, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig optimizeConstantArrays = { { 490 /* optimizeConstantArrays */, 1, 44, 1, 45, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 513 /* countConstantArrayValueTypes */, 1, 46, 1, 47, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig reduceArrays = { { 578 /* reduceArrays */, 2, 48, 2, 50, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectFiniteUnions = { { 614 /* intersectFiniteUnions */, 2, 51, 2, 53, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig finiteUnionMembers = { { 636 /* finiteUnionMembers */, 1, 54, 1, 55, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersect = { { 655 /* intersect */, 0, 56, 1, 57, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig doIntersect = { { 665 /* doIntersect */, 0, 58, 1, 59, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 717 /* intersectDefiniteConstantArrays */, 2, 60, 2, 62, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 779 /* mergeIntersectionsForUnion */, 2, 63, 2, 65, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeFalsey = { { 806 /* removeFalsey */, 1, 66, 1, 67, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeTruthy = { { 819 /* removeTruthy */, 1, 68, 1, 69, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 588 /* getConstantScalarValuesByKey */, 1, 48, 1, 49, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 640 /* haveDisjointConstantScalarValues */, 3, 50, 3, 53, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig reduceArrays = { { 709 /* reduceArrays */, 2, 54, 2, 56, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectFiniteUnions = { { 745 /* intersectFiniteUnions */, 2, 57, 2, 59, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig finiteUnionMembers = { { 767 /* finiteUnionMembers */, 1, 60, 1, 61, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersect = { { 786 /* intersect */, 0, 62, 1, 63, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig doIntersect = { { 796 /* doIntersect */, 0, 64, 1, 65, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 808 /* intersectDefiniteConstantArrays */, 2, 66, 2, 68, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 870 /* mergeIntersectionsForUnion */, 2, 69, 2, 71, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeFalsey = { { 897 /* removeFalsey */, 1, 72, 1, 73, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeTruthy = { { 910 /* removeTruthy */, 1, 74, 1, 75, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::TypeCombinator From d97ab9b9f3d3583084fee53c967150bf043b7cd2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 13:21:08 +0200 Subject: [PATCH 7/9] Intersect finite benevolent unions by identity too intersect() of two unions of finite, mutually disjoint values takes their identity-keyed set intersection instead of distributing one over the other - but only for the exact UnionType class. A benevolent union of constant strings (the values of a large constant array read with an unknown key) narrowed by a falsy check was distributed member by member: 890 constant strings times the seven falsy values, then a union rebuilt from the result, for every condition of a conditional expression holder. A benevolent union takes the fast path too: it is the operand the distribution walks first, so it goes first here and the result keeps the benevolence the distribution would give it. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- src/Type/TypeCombinator.php | 27 +++++++++---- turbo-ext/src/TypeCombinator.cpp | 25 +++++++++--- turbo-ext/src/generated/TypeCombinator.h | 50 +++++++++++++----------- 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 9921d0dd991..6d4ba2dd624 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1744,6 +1744,14 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged * not such a value, in which case the caller falls back to the general A & (B|C) * distribution. */ + /** @phpstan-assert-if-true UnionType $type */ + private static function isPlainOrBenevolentUnion(Type $type): bool + { + $class = get_class($type); + + return $class === UnionType::class || $class === BenevolentUnionType::class; + } + private static function intersectFiniteUnions(UnionType $a, UnionType $b): ?Type { $membersA = self::finiteUnionMembers($a); @@ -1827,19 +1835,24 @@ public static function doIntersect(Type ...$types): Type $unionTypesCount++; } - // Fast path: the intersection of two plain unions whose members are all finite, + // Fast path: the intersection of two unions whose members are all finite, // mutually-disjoint values (constant scalars and/or enum cases) is their // identity-keyed set intersection (O(n)), avoiding the O(n*m) `A & (B|C)` - // distribution + union rebuild below. Restricted to the exact UnionType class so - // BenevolentUnionType and the template union types keep their dedicated handling. + // distribution + union rebuild below. A benevolent union is the one the + // distribution walks first and the benevolence it gives the result is kept; + // the template union types keep their dedicated handling. if ( $typesCount === 2 - && get_class($types[0]) === UnionType::class - && get_class($types[1]) === UnionType::class + && self::isPlainOrBenevolentUnion($types[0]) + && self::isPlainOrBenevolentUnion($types[1]) ) { - $finiteIntersection = self::intersectFiniteUnions($types[0], $types[1]); + [$a, $b] = $types; + if ($b instanceof BenevolentUnionType && !$a instanceof BenevolentUnionType) { + [$a, $b] = [$b, $a]; + } + $finiteIntersection = self::intersectFiniteUnions($a, $b); if ($finiteIntersection !== null) { - return $finiteIntersection; + return $a instanceof BenevolentUnionType ? TypeUtils::toBenevolentUnion($finiteIntersection) : $finiteIntersection; } } diff --git a/turbo-ext/src/TypeCombinator.cpp b/turbo-ext/src/TypeCombinator.cpp index f744bbbedb2..ee155c2085d 100644 --- a/turbo-ext/src/TypeCombinator.cpp +++ b/turbo-ext/src/TypeCombinator.cpp @@ -3166,6 +3166,12 @@ class TypeCombinator return call(newArray.raw(), PT_LC("getarray")); } + /* Mirrors isPlainOrBenevolentUnion() */ + static bool isPlainOrBenevolentUnion(zval *type) + { + return Z_OBJCE_P(type) == pt_ce_union_type || Z_OBJCE_P(type) == pt_ce_benevolent_union_type; + } + static zv::Val doIntersect(uint32_t argc, zval *argv) { size_t typesCount = argc; @@ -3189,13 +3195,20 @@ class TypeCombinator unionTypesCount++; } - /* Fast path: the intersection of two plain unions whose members are - * all finite, mutually-disjoint values is their identity-keyed set - * intersection; restricted to the exact UnionType class */ - if (typesCount == 2 && Z_OBJCE_P(types[0].raw()) == pt_ce_union_type && Z_OBJCE_P(types[1].raw()) == pt_ce_union_type) { - zv::Val finiteIntersection = intersectFiniteUnions(types[0].raw(), types[1].raw()); + /* Fast path: the intersection of two unions whose members are all + * finite, mutually-disjoint values is their identity-keyed set + * intersection; a benevolent union is the one the distribution walks + * first and the benevolence it gives the result is kept */ + if (typesCount == 2 && isPlainOrBenevolentUnion(types[0].raw()) && isPlainOrBenevolentUnion(types[1].raw())) { + zval *a = types[0].raw(); + zval *b = types[1].raw(); + if (Z_OBJCE_P(b) == pt_ce_benevolent_union_type && Z_OBJCE_P(a) != pt_ce_benevolent_union_type) std::swap(a, b); + zv::Val finiteIntersection = intersectFiniteUnions(a, b); PT_FAIL_IF_UNDEF(finiteIntersection); - if (!isNull(finiteIntersection)) return finiteIntersection; + if (!isNull(finiteIntersection)) { + if (Z_OBJCE_P(a) == pt_ce_benevolent_union_type) return pt_union_to_benevolent(finiteIntersection.raw()); + return finiteIntersection; + } } if (unionTypesCount >= 2) { diff --git a/turbo-ext/src/generated/TypeCombinator.h b/turbo-ext/src/generated/TypeCombinator.h index 613d52592e3..303b41cf6d5 100644 --- a/turbo-ext/src/generated/TypeCombinator.h +++ b/turbo-ext/src/generated/TypeCombinator.h @@ -63,16 +63,17 @@ inline constexpr char strings[] = "constantArrays\0" /* 673 */ "preserveTaggedUnions\0" /* 688 */ "reduceArrays\0" /* 709 */ - "PHPStan\\Type\\UnionType\0" /* 722 */ - "intersectFiniteUnions\0" /* 745 */ - "finiteUnionMembers\0" /* 767 */ - "intersect\0" /* 786 */ - "doIntersect\0" /* 796 */ - "intersectDefiniteConstantArrays\0" /* 808 */ - "PHPStan\\Type\\IntersectionType\0" /* 840 */ - "mergeIntersectionsForUnion\0" /* 870 */ - "removeFalsey\0" /* 897 */ - "removeTruthy"; /* 910 */ + "isPlainOrBenevolentUnion\0" /* 722 */ + "PHPStan\\Type\\UnionType\0" /* 747 */ + "intersectFiniteUnions\0" /* 770 */ + "finiteUnionMembers\0" /* 792 */ + "intersect\0" /* 811 */ + "doIntersect\0" /* 821 */ + "intersectDefiniteConstantArrays\0" /* 833 */ + "PHPStan\\Type\\IntersectionType\0" /* 865 */ + "mergeIntersectionsForUnion\0" /* 895 */ + "removeFalsey\0" /* 922 */ + "removeTruthy"; /* 935 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_VOID), /* clearCache return */ reg::packed(12, 0, 17), /* addNull $type */ @@ -131,10 +132,12 @@ inline constexpr reg::PackedArg args[] = { reg::packed(673, MAY_BE_ARRAY), /* reduceArrays $constantArrays */ reg::packed(688, MAY_BE_BOOL), /* reduceArrays $preserveTaggedUnions */ reg::packed(0, MAY_BE_ARRAY), /* reduceArrays return */ - reg::packed(164, 0, 722), /* intersectFiniteUnions $a */ - reg::packed(166, 0, 722), /* intersectFiniteUnions $b */ + reg::packed(12, 0, 17), /* isPlainOrBenevolentUnion $type */ + reg::packed(0, MAY_BE_BOOL), /* isPlainOrBenevolentUnion return */ + reg::packed(164, 0, 747), /* intersectFiniteUnions $a */ + reg::packed(166, 0, 747), /* intersectFiniteUnions $b */ reg::packed(0, MAY_BE_NULL, 17), /* intersectFiniteUnions return */ - reg::packed(132, 0, 722), /* finiteUnionMembers $union */ + reg::packed(132, 0, 747), /* finiteUnionMembers $union */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* finiteUnionMembers return */ reg::packed(126, 0, 17, false, true), /* intersect $types */ reg::packed(0, 0, 17), /* intersect return */ @@ -143,8 +146,8 @@ inline constexpr reg::PackedArg args[] = { reg::packed(164, 0, 548), /* intersectDefiniteConstantArrays $a */ reg::packed(166, 0, 548), /* intersectDefiniteConstantArrays $b */ reg::packed(0, 0, 17), /* intersectDefiniteConstantArrays return */ - reg::packed(164, 0, 840), /* mergeIntersectionsForUnion $a */ - reg::packed(166, 0, 840), /* mergeIntersectionsForUnion $b */ + reg::packed(164, 0, 865), /* mergeIntersectionsForUnion $a */ + reg::packed(166, 0, 865), /* mergeIntersectionsForUnion $b */ reg::packed(0, MAY_BE_NULL, 17), /* mergeIntersectionsForUnion return */ reg::packed(12, 0, 17), /* removeFalsey $type */ reg::packed(0, 0, 17), /* removeFalsey return */ @@ -180,14 +183,15 @@ inline constexpr sigtab::Sig countConstantArrayValueTypes = { { 513 /* countCons inline constexpr sigtab::Sig getConstantScalarValuesByKey = { { 588 /* getConstantScalarValuesByKey */, 1, 48, 1, 49, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig haveDisjointConstantScalarValues = { { 640 /* haveDisjointConstantScalarValues */, 3, 50, 3, 53, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig reduceArrays = { { 709 /* reduceArrays */, 2, 54, 2, 56, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectFiniteUnions = { { 745 /* intersectFiniteUnions */, 2, 57, 2, 59, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig finiteUnionMembers = { { 767 /* finiteUnionMembers */, 1, 60, 1, 61, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersect = { { 786 /* intersect */, 0, 62, 1, 63, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig doIntersect = { { 796 /* doIntersect */, 0, 64, 1, 65, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 808 /* intersectDefiniteConstantArrays */, 2, 66, 2, 68, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 870 /* mergeIntersectionsForUnion */, 2, 69, 2, 71, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeFalsey = { { 897 /* removeFalsey */, 1, 72, 1, 73, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig removeTruthy = { { 910 /* removeTruthy */, 1, 74, 1, 75, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig isPlainOrBenevolentUnion = { { 722 /* isPlainOrBenevolentUnion */, 1, 57, 1, 58, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectFiniteUnions = { { 770 /* intersectFiniteUnions */, 2, 59, 2, 61, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig finiteUnionMembers = { { 792 /* finiteUnionMembers */, 1, 62, 1, 63, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersect = { { 811 /* intersect */, 0, 64, 1, 65, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig doIntersect = { { 821 /* doIntersect */, 0, 66, 1, 67, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig intersectDefiniteConstantArrays = { { 833 /* intersectDefiniteConstantArrays */, 2, 68, 2, 70, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig mergeIntersectionsForUnion = { { 895 /* mergeIntersectionsForUnion */, 2, 71, 2, 73, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeFalsey = { { 922 /* removeFalsey */, 1, 74, 1, 75, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig removeTruthy = { { 935 /* removeTruthy */, 1, 76, 1, 77, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::TypeCombinator From 6888887daf22e2b0f8701e8ab6ce60cbe9cdb684 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 13:21:09 +0200 Subject: [PATCH 8/9] Answer the isset() conjunction fold's left operand once per ask A multi-subject isset() in a non-true context folds its subjects into a chain of conjunctions, each one's left operand being the conjunction folded so far. Every step and every conjunction asked it again with the same scope and context, re-folding every subject before it - 700 specifyConjunction() calls for the 26 subjects of the bench's bug-15061. The left operand of each conjunction answers each (scope, context) pair once; SpecifiedTypes are immutable, so the answer is shared. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Y5roddukgqaSAB2BT765UX --- src/Analyser/ExprHandler/IssetHandler.php | 29 +++++++++++- turbo-ext/src/IssetHandler.cpp | 55 ++++++++++++++++++++++- turbo-ext/src/generated/IssetHandler.h | 8 +++- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index accc87d17c1..684c20df179 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -227,7 +227,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $rightFalseyScope = $accTruthyScope->applySpecifiedTypes($rightTypes($accTruthyScope, TypeSpecifierContext::createFalsey())); $leftExprNode = $accExpr; - $leftTypes = $accTypes; + $leftTypes = self::memoizeSubjectTypes($accTypes); $leftTruthyScope = $accTruthyScope; $leftFalseyScope = $accFalseyScope; $accTypes = fn (MutatingScope $scope, TypeSpecifierContext $ctx): SpecifiedTypes => $this->booleanNarrowingHelper->specifyConjunction( @@ -264,4 +264,31 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } + /** + * The narrowing of the conjunction folded so far: every later conjunction + * asks it again as its left operand, with the scopes and contexts it was + * already asked with - answered once per pair instead of re-folding every + * subject before it on each ask. + * + * @param Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes $types + * @return Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes + */ + private static function memoizeSubjectTypes(Closure $types): Closure + { + $answered = []; + + return static function (MutatingScope $scope, TypeSpecifierContext $context) use ($types, &$answered): SpecifiedTypes { + foreach ($answered as [$answeredScope, $answeredContext, $answer]) { + if ($answeredScope === $scope && $answeredContext === $context) { + return $answer; + } + } + + $answer = $types($scope, $context); + $answered[] = [$scope, $context, $answer]; + + return $answer; + }; + } + } diff --git a/turbo-ext/src/IssetHandler.cpp b/turbo-ext/src/IssetHandler.cpp index 5838821d25f..92a320f3fe0 100644 --- a/turbo-ext/src/IssetHandler.cpp +++ b/turbo-ext/src/IssetHandler.cpp @@ -575,7 +575,8 @@ class IssetHandler ZVAL_COPY_VALUE(&conjunctionCaptures[1], nodeScopeResolver); ZVAL_COPY_VALUE(&conjunctionCaptures[2], expr); ZVAL_COPY_VALUE(&conjunctionCaptures[3], leftExprNode.raw()); - ZVAL_COPY_VALUE(&conjunctionCaptures[4], accTypes.raw()); + zv::Val leftTypes = memoizeSubjectTypes(accTypes.raw()); + ZVAL_COPY_VALUE(&conjunctionCaptures[4], leftTypes.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[5], accTruthyScope.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[6], accFalseyScope.raw()); ZVAL_COPY_VALUE(&conjunctionCaptures[7], rightExprNode.raw()); @@ -675,6 +676,58 @@ class IssetHandler ZVAL_COPY(return_value, &captures[0]); } + /* Mirrors memoizeSubjectTypes(): the closure answering each (scope, + * context) pair once; captures: $types, &$answered */ + static zv::Val memoizeSubjectTypes(zval *types) + { + zval answered; + array_init(&answered); + zval reference; + ZVAL_NEW_REF(&reference, &answered); + zval captures[2]; + ZVAL_COPY_VALUE(&captures[0], types); + ZVAL_COPY_VALUE(&captures[1], &reference); + zv::Val closure = pt_native_closure_new(&memoizedSubjectTypesBody, 2, captures, 1u << 1); + zval_ptr_dtor(&reference); + return closure; + } + + static void memoizedSubjectTypesBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) + { + if (UNEXPECTED(!requireArguments(argc, 2, pt_ish_closure_name))) return; + zval *answered = Z_REFVAL(captures[1]); + if (Z_TYPE_P(answered) == IS_ARRAY && Z_TYPE(argv[0]) == IS_OBJECT && Z_TYPE(argv[1]) == IS_OBJECT) { + zval *entry; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(answered), entry) { + zval *answeredScope = zend_hash_index_find(Z_ARRVAL_P(entry), 0); + zval *answeredContext = zend_hash_index_find(Z_ARRVAL_P(entry), 1); + zval *answer = zend_hash_index_find(Z_ARRVAL_P(entry), 2); + if (answeredScope == NULL || answeredContext == NULL || answer == NULL) continue; + if (Z_OBJ_P(answeredScope) == Z_OBJ(argv[0]) && Z_OBJ_P(answeredContext) == Z_OBJ(argv[1])) { + ZVAL_COPY(return_value, answer); + return; + } + } ZEND_HASH_FOREACH_END(); + } + zv::Val answer = pt_type_call_callable(&captures[0], 2, argv); + if (UNEXPECTED(answer.isUndef())) return; + if (Z_TYPE_P(answered) != IS_ARRAY) { + zval_ptr_dtor(answered); + array_init(answered); + } + SEPARATE_ARRAY(answered); + zval triple; + array_init_size(&triple, 3); + Z_TRY_ADDREF(argv[0]); + zend_hash_next_index_insert(Z_ARRVAL(triple), &argv[0]); + Z_TRY_ADDREF(argv[1]); + zend_hash_next_index_insert(Z_ARRVAL(triple), &argv[1]); + Z_TRY_ADDREF_P(answer.raw()); + zend_hash_next_index_insert(Z_ARRVAL(triple), answer.raw()); + zend_hash_next_index_insert(Z_ARRVAL_P(answered), &triple); + answer.intoReturnValue(return_value); + } + /* fn (MutatingScope $scope, TypeSpecifierContext $ctx): SpecifiedTypes => * $this->booleanNarrowingHelper->specifyConjunction(...) — captures: * $this, $nodeScopeResolver, $expr, $leftExprNode, $leftTypes, diff --git a/turbo-ext/src/generated/IssetHandler.h b/turbo-ext/src/generated/IssetHandler.h index 3cb203bdab8..1f66b94bce5 100644 --- a/turbo-ext/src/generated/IssetHandler.h +++ b/turbo-ext/src/generated/IssetHandler.h @@ -63,7 +63,10 @@ inline constexpr char strings[] = "context\0" /* 613 */ "PHPStan\\Analyser\\ExpressionContext\0" /* 621 */ "PHPStan\\Analyser\\ExpressionResult\0" /* 656 */ - "processExpr"; /* 690 */ + "processExpr\0" /* 690 */ + "types\0" /* 702 */ + "Closure\0" /* 708 */ + "memoizeSubjectTypes"; /* 716 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 21), /* __construct $nonNullabilityHelper */ reg::packed(78, 0, 102), /* __construct $expressionResultFactory */ @@ -80,6 +83,8 @@ inline constexpr reg::PackedArg args[] = { reg::packed(600, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ reg::packed(613, 0, 621), /* processExpr $context */ reg::packed(426, 0, 656), /* processExpr return */ + reg::packed(702, 0, 708), /* memoizeSubjectTypes $types */ + reg::packed(426, 0, 708), /* memoizeSubjectTypes return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -89,6 +94,7 @@ namespace sig { inline constexpr sigtab::Sig __construct = { { 389 /* __construct */, 5, 0, 5, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig supports = { { 427 /* supports */, 1, 5, 1, 6, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig processExpr = { { 690 /* processExpr */, 7, 7, 7, 14, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig memoizeSubjectTypes = { { 716 /* memoizeSubjectTypes */, 1, 15, 1, 16, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::IssetHandler From 7c94037a92949c3a722cb800c6b6ed39744fd4a6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Sep 2026 13:21:20 +0200 Subject: [PATCH 9/9] 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 72f6aef66ca..e77f9fdcba9 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = 'f0ebc74'; + public const EXPECTED_EXTENSION_VERSION = '6888887'; private static bool $active = false;