From 1d13e8e6a1e512ed808a5b8aee6bb376e4606111 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 9 Nov 2026 12:02:38 +0000 Subject: [PATCH] Memoize Scope::getPhpVersion() per scope Since rules and expression handlers ask the scope for the PHP version (8cf6b64ac), PropertyFetchHandler asks on every property fetch and isReadonlyPropertyFetch() on every readonly check. On tests/bench/data/nullsafe-chain-walk.php that is 53,911 calls on 51 distinct scopes, each looking up PHP_VERSION_ID among the expression types and building a new PhpVersions. The answer depends only on the scope's expression types, so the scope remembers it with the array it was computed from. While the expression types are untouched both properties hold the same array and the check is a pointer comparison; a write in place (specifyExpressionTypeInPlace() and co. on a working copy) separates them and the answer is computed again. The native MutatingScope mirrors it. Amp-Thread-ID: https://ampcode.com/threads/T-01a1976c-1bde-72f8-b96c-072279109d59 --- src/Analyser/MutatingScope.php | 21 ++++ .../PHPStan/Analyser/ScopePhpVersionTest.php | 20 ++++ turbo-ext/src/MutatingScope.cpp | 34 +++++- turbo-ext/src/generated/MutatingScope.h | 107 ++++++++++-------- turbo-ext/src/generated/NodeCallbackScope.h | 12 +- 5 files changed, 136 insertions(+), 58 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c7319e2bb99..babb2d54c02 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -177,6 +177,12 @@ class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter, private ?self $scopeWithPromotedNativeTypes = null; + /** What getPhpVersion() answered for the expression types in $phpVersionExpressionTypes. */ + private ?PhpVersions $phpVersion = null; + + /** @var array */ + private array $phpVersionExpressionTypes = []; + /** * @param array $expressionTypes * @param array $conditionalExpressions @@ -5953,6 +5959,21 @@ public function getIterableValueType(Type $iteratee): Type } public function getPhpVersion(): PhpVersions + { + // The same scope is asked over and over - on every property fetch, among others - and the + // answer depends only on its expression types. While they are untouched, both properties + // hold the same array and the comparison is a pointer check; a write in place separates + // them and the answer is computed again. + if ($this->phpVersion !== null && $this->phpVersionExpressionTypes === $this->expressionTypes) { + return $this->phpVersion; + } + + $this->phpVersionExpressionTypes = $this->expressionTypes; + + return $this->phpVersion = $this->resolvePhpVersion(); + } + + private function resolvePhpVersion(): PhpVersions { $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID')); if ($constType !== null && !$this->isOverallPhpVersionRange($constType)) { diff --git a/tests/PHPStan/Analyser/ScopePhpVersionTest.php b/tests/PHPStan/Analyser/ScopePhpVersionTest.php index 0c68a2bc87b..31016881348 100644 --- a/tests/PHPStan/Analyser/ScopePhpVersionTest.php +++ b/tests/PHPStan/Analyser/ScopePhpVersionTest.php @@ -3,8 +3,12 @@ namespace PHPStan\Analyser; use PhpParser\Node; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Exit_; +use PhpParser\Node\Name; use PHPStan\Testing\TypeInferenceTestCase; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntegerType; use PHPStan\Type\VerbosityLevel; use PHPUnit\Framework\Attributes\DataProvider; @@ -39,4 +43,20 @@ public function testPhpVersion(string $expected, string $file): void }); } + public function testPhpVersionFollowsExpressionTypesWrittenInPlace(): void + { + $scope = self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create(__FILE__)); + $unnarrowed = $scope->getPhpVersion()->getType()->describe(VerbosityLevel::precise()); + + $narrowed = $scope->assignExpression( + new ConstFetch(new Name('PHP_VERSION_ID')), + IntegerRangeType::fromInterval(80100, 80199), + new IntegerType(), + ); + $this->assertNotSame('int<80100, 80199>', $unnarrowed); + + $scope->expressionTypes = $narrowed->expressionTypes; + $this->assertSame('int<80100, 80199>', $scope->getPhpVersion()->getType()->describe(VerbosityLevel::precise())); + } + } diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index dfc21fc70bf..7ed45e5e3d0 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -15,9 +15,9 @@ * methods must be declared here — linking checks that. * * Layout. The twin's properties are declared typed property slots in the - * twin's declaration order — the five class-body properties first + * twin's declaration order — the seven class-body properties first * (resolvedTypes, nodeCallbackScope, namespace, scopeOutOfFirstLevelStatement, - * scopeWithPromotedNativeTypes), then the 32 promoted constructor + * scopeWithPromotedNativeTypes, phpVersion, phpVersionExpressionTypes), then the 32 promoted constructor * properties in parameter order ($namespace is the one constructor * parameter that is not promoted) — so the std object handlers do * GC/clone/free and a PHP subclass's own properties follow them. Promoted @@ -300,6 +300,8 @@ enum : uint32_t PT_MS_PROP_NAMESPACE, PT_MS_PROP_SCOPE_OUT_OF_FIRST_LEVEL_STATEMENT, PT_MS_PROP_SCOPE_WITH_PROMOTED_NATIVE_TYPES, + PT_MS_PROP_PHP_VERSION, + PT_MS_PROP_PHP_VERSION_EXPRESSION_TYPES, PT_MS_PROP_SERVICES, PT_MS_PROP_SCOPE_FACTORY, PT_MS_PROP_CONTEXT, @@ -3666,6 +3668,32 @@ class MutatingScope * getFunctionType() (2881) */ zv::Val getPhpVersion() + { + /* the twin's memo: the answer for the expression types it was + * computed from - the same array while they are untouched (the + * pointer check `===` starts with), a separated one after a write in + * place */ + if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val(); + zval *expressionTypes = OBJ_PROP_NUM(self, PT_MS_PROP_EXPRESSION_TYPES); + zval *memoTypes = OBJ_PROP_NUM(self, PT_MS_PROP_PHP_VERSION_EXPRESSION_TYPES); + zv::Ref memo = slot(PT_MS_PROP_PHP_VERSION); + if ( + memo.isObject() + && Z_TYPE_P(memoTypes) == IS_ARRAY + && (Z_ARR_P(memoTypes) == Z_ARR_P(expressionTypes) || zend_is_identical(memoTypes, expressionTypes)) + ) { + return zv::Val::copyOf(memo); + } + + writeSlot(PT_MS_PROP_PHP_VERSION_EXPRESSION_TYPES, zv::Val::copyOf(zv::Ref(expressionTypes))); + zv::Val phpVersion = resolvePhpVersion(); + if (UNEXPECTED(phpVersion.isUndef())) return zv::Val(); + writeSlot(PT_MS_PROP_PHP_VERSION, zv::Val::copyOf(phpVersion.ref())); + return phpVersion; + } + + /* private */ + zv::Val resolvePhpVersion() { zv::Val constantName = zv::Val::string(PT_LC("PHP_VERSION_ID")); zv::Val nameNode = pt_name_node_new(PT_CLASS_NAME, constantName.raw()); @@ -12411,6 +12439,8 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope) cls.property("namespace", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_STRING | MAY_BE_NULL); cls.property("scopeOutOfFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self"); cls.property("scopeWithPromotedNativeTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self"); + cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, phpVersions); + cls.property("phpVersionExpressionTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("services", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, scopeServices); cls.property("scopeFactory", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, internalScopeFactory); cls.property("context", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, scopeContext); diff --git a/turbo-ext/src/generated/MutatingScope.h b/turbo-ext/src/generated/MutatingScope.h index 185b41a21e1..80a6cf5067a 100644 --- a/turbo-ext/src/generated/MutatingScope.h +++ b/turbo-ext/src/generated/MutatingScope.h @@ -15,25 +15,27 @@ inline constexpr uint32_t nodeCallbackScope = 1; inline constexpr uint32_t namespace_ = 2; inline constexpr uint32_t scopeOutOfFirstLevelStatement = 3; inline constexpr uint32_t scopeWithPromotedNativeTypes = 4; -inline constexpr uint32_t services = 5; -inline constexpr uint32_t scopeFactory = 6; -inline constexpr uint32_t context = 7; -inline constexpr uint32_t declareStrictTypes = 8; -inline constexpr uint32_t function = 9; -inline constexpr uint32_t expressionTypes = 10; -inline constexpr uint32_t nativeExpressionTypes = 11; -inline constexpr uint32_t conditionalExpressions = 12; -inline constexpr uint32_t inClosureBindScopeClasses = 13; -inline constexpr uint32_t anonymousFunctionReflection = 14; -inline constexpr uint32_t inFirstLevelStatement = 15; -inline constexpr uint32_t currentlyAssignedExpressions = 16; -inline constexpr uint32_t currentlyAllowedUndefinedExpressions = 17; -inline constexpr uint32_t inFunctionCallsStack = 18; -inline constexpr uint32_t afterExtractCall = 19; -inline constexpr uint32_t parentScope = 20; -inline constexpr uint32_t nativeTypesPromoted = 21; -inline constexpr uint32_t templateArgumentFrame = 22; -inline constexpr uint32_t templateArgumentConstraints = 23; +inline constexpr uint32_t phpVersion = 5; +inline constexpr uint32_t phpVersionExpressionTypes = 6; +inline constexpr uint32_t services = 7; +inline constexpr uint32_t scopeFactory = 8; +inline constexpr uint32_t context = 9; +inline constexpr uint32_t declareStrictTypes = 10; +inline constexpr uint32_t function = 11; +inline constexpr uint32_t expressionTypes = 12; +inline constexpr uint32_t nativeExpressionTypes = 13; +inline constexpr uint32_t conditionalExpressions = 14; +inline constexpr uint32_t inClosureBindScopeClasses = 15; +inline constexpr uint32_t anonymousFunctionReflection = 16; +inline constexpr uint32_t inFirstLevelStatement = 17; +inline constexpr uint32_t currentlyAssignedExpressions = 18; +inline constexpr uint32_t currentlyAllowedUndefinedExpressions = 19; +inline constexpr uint32_t inFunctionCallsStack = 20; +inline constexpr uint32_t afterExtractCall = 21; +inline constexpr uint32_t parentScope = 22; +inline constexpr uint32_t nativeTypesPromoted = 23; +inline constexpr uint32_t templateArgumentFrame = 24; +inline constexpr uint32_t templateArgumentConstraints = 25; } // namespace slot inline void declareClass(reg::Class &cls) @@ -50,6 +52,8 @@ inline void declareProperties(reg::Class &cls) cls.property("namespace", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL | MAY_BE_STRING); cls.property("scopeOutOfFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); cls.property("scopeWithPromotedNativeTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope"); + cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Php\\PhpVersions"); + cls.property("phpVersionExpressionTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY); cls.property("services", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ScopeServices"); cls.property("scopeFactory", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\InternalScopeFactory"); cls.property("context", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ScopeContext"); @@ -482,22 +486,23 @@ inline constexpr char strings[] = "getIterableValueType\0" /* 8547 */ "PHPStan\\Php\\PhpVersions\0" /* 8568 */ "getPhpVersion\0" /* 8592 */ - "getConfiguredPhpVersionRange\0" /* 8606 */ - "isOverallPhpVersionRange\0" /* 8635 */ - "PhpParser\\Node\0" /* 8660 */ - "invokeNodeCallback\0" /* 8675 */ - "collectorType\0" /* 8694 */ - "data\0" /* 8708 */ - "emitCollectedData\0" /* 8713 */ - "extensionClass\0" /* 8731 */ - "trackValueDependency\0" /* 8746 */ - "file\0" /* 8767 */ - "trackFileDependency\0" /* 8772 */ - "directory\0" /* 8792 */ - "pattern\0" /* 8802 */ - "'*'\0" /* 8810 */ - "trackDirectoryDependency\0" /* 8814 */ - "trackClassDependency"; /* 8839 */ + "resolvePhpVersion\0" /* 8606 */ + "getConfiguredPhpVersionRange\0" /* 8624 */ + "isOverallPhpVersionRange\0" /* 8653 */ + "PhpParser\\Node\0" /* 8678 */ + "invokeNodeCallback\0" /* 8693 */ + "collectorType\0" /* 8712 */ + "data\0" /* 8726 */ + "emitCollectedData\0" /* 8731 */ + "extensionClass\0" /* 8749 */ + "trackValueDependency\0" /* 8764 */ + "file\0" /* 8785 */ + "trackFileDependency\0" /* 8790 */ + "directory\0" /* 8810 */ + "pattern\0" /* 8820 */ + "'*'\0" /* 8828 */ + "trackDirectoryDependency\0" /* 8832 */ + "trackClassDependency"; /* 8857 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 9), /* __construct $services */ reg::packed(40, 0, 53), /* __construct $scopeFactory */ @@ -1016,21 +1021,22 @@ inline constexpr reg::PackedArg args[] = { reg::packed(5138, 0, 1424), /* getIterableValueType $iteratee */ reg::packed(738, 0, 1424), /* getIterableValueType return */ reg::packed(738, 0, 8568), /* getPhpVersion return */ + reg::packed(738, 0, 8568), /* resolvePhpVersion return */ reg::packed(738, MAY_BE_NULL, 1424), /* getConfiguredPhpVersionRange return */ reg::packed(3874, 0, 1424), /* isOverallPhpVersionRange $type */ reg::packed(738, MAY_BE_BOOL), /* isOverallPhpVersionRange return */ - reg::packed(1707, 0, 8660), /* invokeNodeCallback $node */ + reg::packed(1707, 0, 8678), /* invokeNodeCallback $node */ reg::packed(738, MAY_BE_VOID), /* invokeNodeCallback return */ - reg::packed(8694, MAY_BE_STRING), /* emitCollectedData $collectorType */ - reg::packed(8708, MAY_BE_ANY), /* emitCollectedData $data */ + reg::packed(8712, MAY_BE_STRING), /* emitCollectedData $collectorType */ + reg::packed(8726, MAY_BE_ANY), /* emitCollectedData $data */ reg::packed(738, MAY_BE_VOID), /* emitCollectedData return */ - reg::packed(8731, MAY_BE_STRING), /* trackValueDependency $extensionClass */ + reg::packed(8749, MAY_BE_STRING), /* trackValueDependency $extensionClass */ reg::packed(2540, MAY_BE_STRING), /* trackValueDependency $key */ reg::packed(738, MAY_BE_VOID), /* trackValueDependency return */ - reg::packed(8767, MAY_BE_STRING), /* trackFileDependency $file */ + reg::packed(8785, MAY_BE_STRING), /* trackFileDependency $file */ reg::packed(738, MAY_BE_VOID), /* trackFileDependency return */ - reg::packed(8792, MAY_BE_STRING), /* trackDirectoryDependency $directory */ - reg::packed(8802, MAY_BE_STRING, reg::NoString, false, false, 8810), /* trackDirectoryDependency $pattern */ + reg::packed(8810, MAY_BE_STRING), /* trackDirectoryDependency $directory */ + reg::packed(8820, MAY_BE_STRING, reg::NoString, false, false, 8828), /* trackDirectoryDependency $pattern */ reg::packed(738, MAY_BE_VOID), /* trackDirectoryDependency return */ reg::packed(2977, MAY_BE_STRING), /* trackClassDependency $className */ reg::packed(738, MAY_BE_VOID), /* trackClassDependency return */ @@ -1233,14 +1239,15 @@ inline constexpr sigtab::Sig getNativeConstantTypes = { { 8505 /* getNativeConst inline constexpr sigtab::Sig getIterableKeyType = { { 8528 /* getIterableKeyType */, 1, 512, 1, 513, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getIterableValueType = { { 8547 /* getIterableValueType */, 1, 514, 1, 515, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getPhpVersion = { { 8592 /* getPhpVersion */, 0, 516, 0, 516, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getConfiguredPhpVersionRange = { { 8606 /* getConfiguredPhpVersionRange */, 0, 517, 0, 517, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 8635 /* isOverallPhpVersionRange */, 1, 518, 1, 519, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig invokeNodeCallback = { { 8675 /* invokeNodeCallback */, 1, 520, 1, 521, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig emitCollectedData = { { 8713 /* emitCollectedData */, 2, 522, 2, 524, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackValueDependency = { { 8746 /* trackValueDependency */, 2, 525, 2, 527, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackFileDependency = { { 8772 /* trackFileDependency */, 1, 528, 1, 529, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackDirectoryDependency = { { 8814 /* trackDirectoryDependency */, 1, 530, 2, 532, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig trackClassDependency = { { 8839 /* trackClassDependency */, 1, 533, 1, 534, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolvePhpVersion = { { 8606 /* resolvePhpVersion */, 0, 517, 0, 517, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getConfiguredPhpVersionRange = { { 8624 /* getConfiguredPhpVersionRange */, 0, 518, 0, 518, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 8653 /* isOverallPhpVersionRange */, 1, 519, 1, 520, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig invokeNodeCallback = { { 8693 /* invokeNodeCallback */, 1, 521, 1, 522, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig emitCollectedData = { { 8731 /* emitCollectedData */, 2, 523, 2, 525, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackValueDependency = { { 8764 /* trackValueDependency */, 2, 526, 2, 528, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackFileDependency = { { 8790 /* trackFileDependency */, 1, 529, 1, 530, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackDirectoryDependency = { { 8832 /* trackDirectoryDependency */, 1, 531, 2, 533, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig trackClassDependency = { { 8857 /* trackClassDependency */, 1, 534, 1, 535, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::MutatingScope diff --git a/turbo-ext/src/generated/NodeCallbackScope.h b/turbo-ext/src/generated/NodeCallbackScope.h index 5a8723a79d7..251332f8002 100644 --- a/turbo-ext/src/generated/NodeCallbackScope.h +++ b/turbo-ext/src/generated/NodeCallbackScope.h @@ -10,12 +10,12 @@ namespace ptdecl::NodeCallbackScope { /* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ namespace slot { -inline constexpr uint32_t truthyValueExprs = 24; -inline constexpr uint32_t falseyValueExprs = 25; -inline constexpr uint32_t walkScope = 26; -inline constexpr uint32_t seededWalkScope = 27; -inline constexpr uint32_t askedTypes = 28; -inline constexpr uint32_t askedNativeTypes = 29; +inline constexpr uint32_t truthyValueExprs = 26; +inline constexpr uint32_t falseyValueExprs = 27; +inline constexpr uint32_t walkScope = 28; +inline constexpr uint32_t seededWalkScope = 29; +inline constexpr uint32_t askedTypes = 30; +inline constexpr uint32_t askedNativeTypes = 31; } // namespace slot inline void declareClass(reg::Class &cls)