Skip to content

Commit ccd2611

Browse files
committed
Memoize Scope::getPhpVersion() per scope
Since rules and expression handlers ask the scope for the PHP version (8cf6b64), 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
1 parent a45093f commit ccd2611

5 files changed

Lines changed: 136 additions & 58 deletions

File tree

‎src/Analyser/MutatingScope.php‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter,
177177

178178
private ?self $scopeWithPromotedNativeTypes = null;
179179

180+
/** What getPhpVersion() answered for the expression types in $phpVersionExpressionTypes. */
181+
private ?PhpVersions $phpVersion = null;
182+
183+
/** @var array<string, ExpressionTypeHolder> */
184+
private array $phpVersionExpressionTypes = [];
185+
180186
/**
181187
* @param array<string, ExpressionTypeHolder> $expressionTypes
182188
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
@@ -5953,6 +5959,21 @@ public function getIterableValueType(Type $iteratee): Type
59535959
}
59545960

59555961
public function getPhpVersion(): PhpVersions
5962+
{
5963+
// The same scope is asked over and over - on every property fetch, among others - and the
5964+
// answer depends only on its expression types. While they are untouched, both properties
5965+
// hold the same array and the comparison is a pointer check; a write in place separates
5966+
// them and the answer is computed again.
5967+
if ($this->phpVersion !== null && $this->phpVersionExpressionTypes === $this->expressionTypes) {
5968+
return $this->phpVersion;
5969+
}
5970+
5971+
$this->phpVersionExpressionTypes = $this->expressionTypes;
5972+
5973+
return $this->phpVersion = $this->resolvePhpVersion();
5974+
}
5975+
5976+
private function resolvePhpVersion(): PhpVersions
59565977
{
59575978
$constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID'));
59585979
if ($constType !== null && !$this->isOverallPhpVersionRange($constType)) {

‎tests/PHPStan/Analyser/ScopePhpVersionTest.php‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
namespace PHPStan\Analyser;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Expr\ConstFetch;
67
use PhpParser\Node\Expr\Exit_;
8+
use PhpParser\Node\Name;
79
use PHPStan\Testing\TypeInferenceTestCase;
10+
use PHPStan\Type\IntegerRangeType;
11+
use PHPStan\Type\IntegerType;
812
use PHPStan\Type\VerbosityLevel;
913
use PHPUnit\Framework\Attributes\DataProvider;
1014

@@ -39,4 +43,20 @@ public function testPhpVersion(string $expected, string $file): void
3943
});
4044
}
4145

46+
public function testPhpVersionFollowsExpressionTypesWrittenInPlace(): void
47+
{
48+
$scope = self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create(__FILE__));
49+
$unnarrowed = $scope->getPhpVersion()->getType()->describe(VerbosityLevel::precise());
50+
51+
$narrowed = $scope->assignExpression(
52+
new ConstFetch(new Name('PHP_VERSION_ID')),
53+
IntegerRangeType::fromInterval(80100, 80199),
54+
new IntegerType(),
55+
);
56+
$this->assertNotSame('int<80100, 80199>', $unnarrowed);
57+
58+
$scope->expressionTypes = $narrowed->expressionTypes;
59+
$this->assertSame('int<80100, 80199>', $scope->getPhpVersion()->getType()->describe(VerbosityLevel::precise()));
60+
}
61+
4262
}

‎turbo-ext/src/MutatingScope.cpp‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
* methods must be declared here — linking checks that.
1616
*
1717
* Layout. The twin's properties are declared typed property slots in the
18-
* twin's declaration order — the five class-body properties first
18+
* twin's declaration order — the seven class-body properties first
1919
* (resolvedTypes, nodeCallbackScope, namespace, scopeOutOfFirstLevelStatement,
20-
* scopeWithPromotedNativeTypes), then the 32 promoted constructor
20+
* scopeWithPromotedNativeTypes, phpVersion, phpVersionExpressionTypes), then the 32 promoted constructor
2121
* properties in parameter order ($namespace is the one constructor
2222
* parameter that is not promoted) — so the std object handlers do
2323
* GC/clone/free and a PHP subclass's own properties follow them. Promoted
@@ -300,6 +300,8 @@ enum : uint32_t
300300
PT_MS_PROP_NAMESPACE,
301301
PT_MS_PROP_SCOPE_OUT_OF_FIRST_LEVEL_STATEMENT,
302302
PT_MS_PROP_SCOPE_WITH_PROMOTED_NATIVE_TYPES,
303+
PT_MS_PROP_PHP_VERSION,
304+
PT_MS_PROP_PHP_VERSION_EXPRESSION_TYPES,
303305
PT_MS_PROP_SERVICES,
304306
PT_MS_PROP_SCOPE_FACTORY,
305307
PT_MS_PROP_CONTEXT,
@@ -3666,6 +3668,32 @@ class MutatingScope
36663668
* getFunctionType() (2881) */
36673669

36683670
zv::Val getPhpVersion()
3671+
{
3672+
/* the twin's memo: the answer for the expression types it was
3673+
* computed from - the same array while they are untouched (the
3674+
* pointer check `===` starts with), a separated one after a write in
3675+
* place */
3676+
if (UNEXPECTED(!requireSlot(PT_MS_PROP_EXPRESSION_TYPES, "expressionTypes"))) return zv::Val();
3677+
zval *expressionTypes = OBJ_PROP_NUM(self, PT_MS_PROP_EXPRESSION_TYPES);
3678+
zval *memoTypes = OBJ_PROP_NUM(self, PT_MS_PROP_PHP_VERSION_EXPRESSION_TYPES);
3679+
zv::Ref memo = slot(PT_MS_PROP_PHP_VERSION);
3680+
if (
3681+
memo.isObject()
3682+
&& Z_TYPE_P(memoTypes) == IS_ARRAY
3683+
&& (Z_ARR_P(memoTypes) == Z_ARR_P(expressionTypes) || zend_is_identical(memoTypes, expressionTypes))
3684+
) {
3685+
return zv::Val::copyOf(memo);
3686+
}
3687+
3688+
writeSlot(PT_MS_PROP_PHP_VERSION_EXPRESSION_TYPES, zv::Val::copyOf(zv::Ref(expressionTypes)));
3689+
zv::Val phpVersion = resolvePhpVersion();
3690+
if (UNEXPECTED(phpVersion.isUndef())) return zv::Val();
3691+
writeSlot(PT_MS_PROP_PHP_VERSION, zv::Val::copyOf(phpVersion.ref()));
3692+
return phpVersion;
3693+
}
3694+
3695+
/* private */
3696+
zv::Val resolvePhpVersion()
36693697
{
36703698
zv::Val constantName = zv::Val::string(PT_LC("PHP_VERSION_ID"));
36713699
zv::Val nameNode = pt_name_node_new(PT_CLASS_NAME, constantName.raw());
@@ -12411,6 +12439,8 @@ PT_MINIT_REGISTRATION(pt_register_mutating_scope)
1241112439
cls.property("namespace", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_STRING | MAY_BE_NULL);
1241212440
cls.property("scopeOutOfFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self");
1241312441
cls.property("scopeWithPromotedNativeTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "self");
12442+
cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, phpVersions);
12443+
cls.property("phpVersionExpressionTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY);
1241412444
cls.property("services", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, scopeServices);
1241512445
cls.property("scopeFactory", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, internalScopeFactory);
1241612446
cls.property("context", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, scopeContext);

‎turbo-ext/src/generated/MutatingScope.h‎

Lines changed: 57 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,27 @@ inline constexpr uint32_t nodeCallbackScope = 1;
1515
inline constexpr uint32_t namespace_ = 2;
1616
inline constexpr uint32_t scopeOutOfFirstLevelStatement = 3;
1717
inline constexpr uint32_t scopeWithPromotedNativeTypes = 4;
18-
inline constexpr uint32_t services = 5;
19-
inline constexpr uint32_t scopeFactory = 6;
20-
inline constexpr uint32_t context = 7;
21-
inline constexpr uint32_t declareStrictTypes = 8;
22-
inline constexpr uint32_t function = 9;
23-
inline constexpr uint32_t expressionTypes = 10;
24-
inline constexpr uint32_t nativeExpressionTypes = 11;
25-
inline constexpr uint32_t conditionalExpressions = 12;
26-
inline constexpr uint32_t inClosureBindScopeClasses = 13;
27-
inline constexpr uint32_t anonymousFunctionReflection = 14;
28-
inline constexpr uint32_t inFirstLevelStatement = 15;
29-
inline constexpr uint32_t currentlyAssignedExpressions = 16;
30-
inline constexpr uint32_t currentlyAllowedUndefinedExpressions = 17;
31-
inline constexpr uint32_t inFunctionCallsStack = 18;
32-
inline constexpr uint32_t afterExtractCall = 19;
33-
inline constexpr uint32_t parentScope = 20;
34-
inline constexpr uint32_t nativeTypesPromoted = 21;
35-
inline constexpr uint32_t templateArgumentFrame = 22;
36-
inline constexpr uint32_t templateArgumentConstraints = 23;
18+
inline constexpr uint32_t phpVersion = 5;
19+
inline constexpr uint32_t phpVersionExpressionTypes = 6;
20+
inline constexpr uint32_t services = 7;
21+
inline constexpr uint32_t scopeFactory = 8;
22+
inline constexpr uint32_t context = 9;
23+
inline constexpr uint32_t declareStrictTypes = 10;
24+
inline constexpr uint32_t function = 11;
25+
inline constexpr uint32_t expressionTypes = 12;
26+
inline constexpr uint32_t nativeExpressionTypes = 13;
27+
inline constexpr uint32_t conditionalExpressions = 14;
28+
inline constexpr uint32_t inClosureBindScopeClasses = 15;
29+
inline constexpr uint32_t anonymousFunctionReflection = 16;
30+
inline constexpr uint32_t inFirstLevelStatement = 17;
31+
inline constexpr uint32_t currentlyAssignedExpressions = 18;
32+
inline constexpr uint32_t currentlyAllowedUndefinedExpressions = 19;
33+
inline constexpr uint32_t inFunctionCallsStack = 20;
34+
inline constexpr uint32_t afterExtractCall = 21;
35+
inline constexpr uint32_t parentScope = 22;
36+
inline constexpr uint32_t nativeTypesPromoted = 23;
37+
inline constexpr uint32_t templateArgumentFrame = 24;
38+
inline constexpr uint32_t templateArgumentConstraints = 25;
3739
} // namespace slot
3840

3941
inline void declareClass(reg::Class &cls)
@@ -50,6 +52,8 @@ inline void declareProperties(reg::Class &cls)
5052
cls.property("namespace", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL | MAY_BE_STRING);
5153
cls.property("scopeOutOfFirstLevelStatement", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope");
5254
cls.property("scopeWithPromotedNativeTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Analyser\\MutatingScope");
55+
cls.property("phpVersion", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedNull, MAY_BE_NULL, "PHPStan\\Php\\PhpVersions");
56+
cls.property("phpVersionExpressionTypes", ZEND_ACC_PRIVATE, reg::PropertyKind::TypedEmptyArray, MAY_BE_ARRAY);
5357
cls.property("services", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ScopeServices");
5458
cls.property("scopeFactory", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\InternalScopeFactory");
5559
cls.property("context", ZEND_ACC_PROTECTED, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ScopeContext");
@@ -482,22 +486,23 @@ inline constexpr char strings[] =
482486
"getIterableValueType\0" /* 8547 */
483487
"PHPStan\\Php\\PhpVersions\0" /* 8568 */
484488
"getPhpVersion\0" /* 8592 */
485-
"getConfiguredPhpVersionRange\0" /* 8606 */
486-
"isOverallPhpVersionRange\0" /* 8635 */
487-
"PhpParser\\Node\0" /* 8660 */
488-
"invokeNodeCallback\0" /* 8675 */
489-
"collectorType\0" /* 8694 */
490-
"data\0" /* 8708 */
491-
"emitCollectedData\0" /* 8713 */
492-
"extensionClass\0" /* 8731 */
493-
"trackValueDependency\0" /* 8746 */
494-
"file\0" /* 8767 */
495-
"trackFileDependency\0" /* 8772 */
496-
"directory\0" /* 8792 */
497-
"pattern\0" /* 8802 */
498-
"'*'\0" /* 8810 */
499-
"trackDirectoryDependency\0" /* 8814 */
500-
"trackClassDependency"; /* 8839 */
489+
"resolvePhpVersion\0" /* 8606 */
490+
"getConfiguredPhpVersionRange\0" /* 8624 */
491+
"isOverallPhpVersionRange\0" /* 8653 */
492+
"PhpParser\\Node\0" /* 8678 */
493+
"invokeNodeCallback\0" /* 8693 */
494+
"collectorType\0" /* 8712 */
495+
"data\0" /* 8726 */
496+
"emitCollectedData\0" /* 8731 */
497+
"extensionClass\0" /* 8749 */
498+
"trackValueDependency\0" /* 8764 */
499+
"file\0" /* 8785 */
500+
"trackFileDependency\0" /* 8790 */
501+
"directory\0" /* 8810 */
502+
"pattern\0" /* 8820 */
503+
"'*'\0" /* 8828 */
504+
"trackDirectoryDependency\0" /* 8832 */
505+
"trackClassDependency"; /* 8857 */
501506
inline constexpr reg::PackedArg args[] = {
502507
reg::packed(0, 0, 9), /* __construct $services */
503508
reg::packed(40, 0, 53), /* __construct $scopeFactory */
@@ -1016,21 +1021,22 @@ inline constexpr reg::PackedArg args[] = {
10161021
reg::packed(5138, 0, 1424), /* getIterableValueType $iteratee */
10171022
reg::packed(738, 0, 1424), /* getIterableValueType return */
10181023
reg::packed(738, 0, 8568), /* getPhpVersion return */
1024+
reg::packed(738, 0, 8568), /* resolvePhpVersion return */
10191025
reg::packed(738, MAY_BE_NULL, 1424), /* getConfiguredPhpVersionRange return */
10201026
reg::packed(3874, 0, 1424), /* isOverallPhpVersionRange $type */
10211027
reg::packed(738, MAY_BE_BOOL), /* isOverallPhpVersionRange return */
1022-
reg::packed(1707, 0, 8660), /* invokeNodeCallback $node */
1028+
reg::packed(1707, 0, 8678), /* invokeNodeCallback $node */
10231029
reg::packed(738, MAY_BE_VOID), /* invokeNodeCallback return */
1024-
reg::packed(8694, MAY_BE_STRING), /* emitCollectedData $collectorType */
1025-
reg::packed(8708, MAY_BE_ANY), /* emitCollectedData $data */
1030+
reg::packed(8712, MAY_BE_STRING), /* emitCollectedData $collectorType */
1031+
reg::packed(8726, MAY_BE_ANY), /* emitCollectedData $data */
10261032
reg::packed(738, MAY_BE_VOID), /* emitCollectedData return */
1027-
reg::packed(8731, MAY_BE_STRING), /* trackValueDependency $extensionClass */
1033+
reg::packed(8749, MAY_BE_STRING), /* trackValueDependency $extensionClass */
10281034
reg::packed(2540, MAY_BE_STRING), /* trackValueDependency $key */
10291035
reg::packed(738, MAY_BE_VOID), /* trackValueDependency return */
1030-
reg::packed(8767, MAY_BE_STRING), /* trackFileDependency $file */
1036+
reg::packed(8785, MAY_BE_STRING), /* trackFileDependency $file */
10311037
reg::packed(738, MAY_BE_VOID), /* trackFileDependency return */
1032-
reg::packed(8792, MAY_BE_STRING), /* trackDirectoryDependency $directory */
1033-
reg::packed(8802, MAY_BE_STRING, reg::NoString, false, false, 8810), /* trackDirectoryDependency $pattern */
1038+
reg::packed(8810, MAY_BE_STRING), /* trackDirectoryDependency $directory */
1039+
reg::packed(8820, MAY_BE_STRING, reg::NoString, false, false, 8828), /* trackDirectoryDependency $pattern */
10341040
reg::packed(738, MAY_BE_VOID), /* trackDirectoryDependency return */
10351041
reg::packed(2977, MAY_BE_STRING), /* trackClassDependency $className */
10361042
reg::packed(738, MAY_BE_VOID), /* trackClassDependency return */
@@ -1233,14 +1239,15 @@ inline constexpr sigtab::Sig getNativeConstantTypes = { { 8505 /* getNativeConst
12331239
inline constexpr sigtab::Sig getIterableKeyType = { { 8528 /* getIterableKeyType */, 1, 512, 1, 513, ZEND_ACC_PUBLIC } };
12341240
inline constexpr sigtab::Sig getIterableValueType = { { 8547 /* getIterableValueType */, 1, 514, 1, 515, ZEND_ACC_PUBLIC } };
12351241
inline constexpr sigtab::Sig getPhpVersion = { { 8592 /* getPhpVersion */, 0, 516, 0, 516, ZEND_ACC_PUBLIC } };
1236-
inline constexpr sigtab::Sig getConfiguredPhpVersionRange = { { 8606 /* getConfiguredPhpVersionRange */, 0, 517, 0, 517, ZEND_ACC_PRIVATE } };
1237-
inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 8635 /* isOverallPhpVersionRange */, 1, 518, 1, 519, ZEND_ACC_PRIVATE } };
1238-
inline constexpr sigtab::Sig invokeNodeCallback = { { 8675 /* invokeNodeCallback */, 1, 520, 1, 521, ZEND_ACC_PUBLIC } };
1239-
inline constexpr sigtab::Sig emitCollectedData = { { 8713 /* emitCollectedData */, 2, 522, 2, 524, ZEND_ACC_PUBLIC } };
1240-
inline constexpr sigtab::Sig trackValueDependency = { { 8746 /* trackValueDependency */, 2, 525, 2, 527, ZEND_ACC_PUBLIC } };
1241-
inline constexpr sigtab::Sig trackFileDependency = { { 8772 /* trackFileDependency */, 1, 528, 1, 529, ZEND_ACC_PUBLIC } };
1242-
inline constexpr sigtab::Sig trackDirectoryDependency = { { 8814 /* trackDirectoryDependency */, 1, 530, 2, 532, ZEND_ACC_PUBLIC } };
1243-
inline constexpr sigtab::Sig trackClassDependency = { { 8839 /* trackClassDependency */, 1, 533, 1, 534, ZEND_ACC_PUBLIC } };
1242+
inline constexpr sigtab::Sig resolvePhpVersion = { { 8606 /* resolvePhpVersion */, 0, 517, 0, 517, ZEND_ACC_PRIVATE } };
1243+
inline constexpr sigtab::Sig getConfiguredPhpVersionRange = { { 8624 /* getConfiguredPhpVersionRange */, 0, 518, 0, 518, ZEND_ACC_PRIVATE } };
1244+
inline constexpr sigtab::Sig isOverallPhpVersionRange = { { 8653 /* isOverallPhpVersionRange */, 1, 519, 1, 520, ZEND_ACC_PRIVATE } };
1245+
inline constexpr sigtab::Sig invokeNodeCallback = { { 8693 /* invokeNodeCallback */, 1, 521, 1, 522, ZEND_ACC_PUBLIC } };
1246+
inline constexpr sigtab::Sig emitCollectedData = { { 8731 /* emitCollectedData */, 2, 523, 2, 525, ZEND_ACC_PUBLIC } };
1247+
inline constexpr sigtab::Sig trackValueDependency = { { 8764 /* trackValueDependency */, 2, 526, 2, 528, ZEND_ACC_PUBLIC } };
1248+
inline constexpr sigtab::Sig trackFileDependency = { { 8790 /* trackFileDependency */, 1, 529, 1, 530, ZEND_ACC_PUBLIC } };
1249+
inline constexpr sigtab::Sig trackDirectoryDependency = { { 8832 /* trackDirectoryDependency */, 1, 531, 2, 533, ZEND_ACC_PUBLIC } };
1250+
inline constexpr sigtab::Sig trackClassDependency = { { 8857 /* trackClassDependency */, 1, 534, 1, 535, ZEND_ACC_PUBLIC } };
12441251
} // namespace sig
12451252

12461253
} // namespace ptdecl::MutatingScope

‎turbo-ext/src/generated/NodeCallbackScope.h‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ namespace ptdecl::NodeCallbackScope {
1010

1111
/* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */
1212
namespace slot {
13-
inline constexpr uint32_t truthyValueExprs = 24;
14-
inline constexpr uint32_t falseyValueExprs = 25;
15-
inline constexpr uint32_t walkScope = 26;
16-
inline constexpr uint32_t seededWalkScope = 27;
17-
inline constexpr uint32_t askedTypes = 28;
18-
inline constexpr uint32_t askedNativeTypes = 29;
13+
inline constexpr uint32_t truthyValueExprs = 26;
14+
inline constexpr uint32_t falseyValueExprs = 27;
15+
inline constexpr uint32_t walkScope = 28;
16+
inline constexpr uint32_t seededWalkScope = 29;
17+
inline constexpr uint32_t askedTypes = 30;
18+
inline constexpr uint32_t askedNativeTypes = 31;
1919
} // namespace slot
2020

2121
inline void declareClass(reg::Class &cls)

0 commit comments

Comments
 (0)