Skip to content

Commit bd07af4

Browse files
committed
Do the per-key work of array shapes once per shared value type
Writing through a union offset gives an array shape one optional key per member of the offset, all holding the same value type. With 250 keys and a 250-member value type, every operation that walked the keys did the same work 250 times: - mergeWith() and legacyMergeWith() unioned each pair of value types once per key; they now union each distinct pair once, so the merged keys keep sharing one value type - isSuperTypeOf() against another shape called getOffsetValueType() and isSuperTypeOf() once per key; it now compares each pair of value types once - countConstantArrayValueTypes() traversed a shared value type once per key; it now traverses it once and counts it per occurrence - reduceArrays()' single-constant-scalar lookup inspected the shared value type once per key - getValuesArray() and getKeysArray() unioned every window of possible source values from scratch, quadratic in the number of keys; the window unions are now kept as a two-stack queue, linear in the number of keys The inferred types do not change. The turbo-ext mirrors get the same changes. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b24f-e4cc-7457-9324-5d29caf3c481
1 parent 26e6270 commit bd07af4

6 files changed

Lines changed: 559 additions & 45 deletions

File tree

‎src/Type/Constant/ConstantArrayType.php‎

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
809809
}
810810

811811
$results = [];
812+
$valueResults = [];
812813
foreach ($this->keyTypes as $i => $keyType) {
813814
$hasOffset = $type->hasOffsetValueType($keyType);
814815
if ($bothDefinite && $hasOffset->no() && $typeUnsealedness->yes()) {
@@ -831,12 +832,26 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
831832
$results[] = IsSuperTypeOfResult::createMaybe();
832833
}
833834

834-
$otherValueType = $type->getOffsetValueType($keyType);
835-
if ($otherValueType instanceof ErrorType && $bothDefinite && $typeUnsealedness->yes()) {
836-
[, $typeUnsealedValue] = $type->unsealed;
837-
$otherValueType = $typeUnsealedValue;
835+
// The offset value at one of $type's explicit keys depends only on
836+
// the value type stored there, so keys sharing their value types on
837+
// both sides (keys written through a union offset) compare them once.
838+
$otherIndex = $type->getKeyIndex($keyType);
839+
$valuePairKey = $otherIndex !== null
840+
? spl_object_id($this->valueTypes[$i]) . ':' . spl_object_id($type->valueTypes[$otherIndex])
841+
: null;
842+
if ($valuePairKey !== null && array_key_exists($valuePairKey, $valueResults)) {
843+
$isValueSuperType = $valueResults[$valuePairKey];
844+
} else {
845+
$otherValueType = $type->getOffsetValueType($keyType);
846+
if ($otherValueType instanceof ErrorType && $bothDefinite && $typeUnsealedness->yes()) {
847+
[, $typeUnsealedValue] = $type->unsealed;
848+
$otherValueType = $typeUnsealedValue;
849+
}
850+
$isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($otherValueType);
851+
if ($valuePairKey !== null) {
852+
$valueResults[$valuePairKey] = $isValueSuperType;
853+
}
838854
}
839-
$isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($otherValueType);
840855
if ($isValueSuperType->no()) {
841856
return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason));
842857
}
@@ -2579,6 +2594,18 @@ private function getKeysOrValuesArray(array $types, ?Type $unsealedSourceType):
25792594
$optionalKeys = [];
25802595
$maxIndex = 0;
25812596

2597+
// Position $i of the result can hold any source value from $i up to
2598+
// $maxIndex. Both ends of that window only move forward, so the
2599+
// window unions are kept as a two-stack queue: $suffixUnions[$j] is
2600+
// the union of $types[$j..$suffixEnd] and $tailUnion the union of
2601+
// the values added after $suffixEnd. Each value enters the suffix
2602+
// unions once, which keeps this linear in the number of keys instead
2603+
// of unioning every window from scratch.
2604+
$suffixUnions = [];
2605+
$suffixEnd = -1;
2606+
$tailUnion = null;
2607+
$tailEnd = -1;
2608+
25822609
foreach ($types as $i => $type) {
25832610
$keyTypes[] = new ConstantIntegerType($i);
25842611

@@ -2592,7 +2619,26 @@ private function getKeysOrValuesArray(array $types, ?Type $unsealedSourceType):
25922619
if ($i === $maxIndex) {
25932620
$valueTypes[] = $type;
25942621
} else {
2595-
$valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1));
2622+
$windowEnd = min($maxIndex, $count - 1);
2623+
if ($i === $windowEnd) {
2624+
$valueTypes[] = TypeCombinator::union($type);
2625+
} else {
2626+
if ($i > $suffixEnd) {
2627+
$suffixEnd = $windowEnd;
2628+
$suffixUnions = [$suffixEnd => $types[$suffixEnd]];
2629+
for ($j = $suffixEnd - 1; $j >= $i; $j--) {
2630+
$suffixUnions[$j] = TypeCombinator::union($types[$j], $suffixUnions[$j + 1]);
2631+
}
2632+
$tailUnion = null;
2633+
$tailEnd = $suffixEnd;
2634+
}
2635+
for ($j = $tailEnd + 1; $j <= $windowEnd; $j++) {
2636+
$tailUnion = $tailUnion === null ? $types[$j] : TypeCombinator::union($tailUnion, $types[$j]);
2637+
}
2638+
$tailEnd = $windowEnd;
2639+
2640+
$valueTypes[] = $tailUnion === null ? $suffixUnions[$i] : TypeCombinator::union($suffixUnions[$i], $tailUnion);
2641+
}
25962642
if ($maxIndex >= $count) {
25972643
$optionalKeys[] = $i;
25982644
}
@@ -3155,6 +3201,10 @@ public function mergeWith(self $otherArray): self
31553201

31563202
$otherKeyIndexMap = $otherArray->getKeyIndexMap();
31573203
$processed = [];
3204+
// Keys written through a union offset share their value type: union
3205+
// each pair of value types once, which also keeps the merged keys
3206+
// sharing the result.
3207+
$mergedValues = [];
31583208

31593209
foreach ($this->keyTypes as $i => $keyType) {
31603210
$keyValue = $keyType->getValue();
@@ -3164,7 +3214,7 @@ public function mergeWith(self $otherArray): self
31643214
if (array_key_exists($keyValue, $otherKeyIndexMap)) {
31653215
$j = $otherKeyIndexMap[$keyValue];
31663216
$otherValueType = $otherArray->valueTypes[$j];
3167-
$mergedValue = TypeCombinator::union($valueType, $otherValueType);
3217+
$mergedValue = $mergedValues[spl_object_id($valueType) . ':' . spl_object_id($otherValueType)] ??= TypeCombinator::union($valueType, $otherValueType);
31683218
$optional = $this->isOptionalKey($i) || $otherArray->isOptionalKey($j);
31693219

31703220
$keyTypes[] = $keyType;
@@ -3238,6 +3288,8 @@ private function legacyMergeWith(self $otherArray): self
32383288
{
32393289
$valueTypes = $this->valueTypes;
32403290
$optionalKeys = $this->optionalKeys;
3291+
// union each pair of value types once, see mergeWith()
3292+
$mergedValues = [];
32413293
foreach ($this->keyTypes as $i => $keyType) {
32423294
$otherIndex = $otherArray->getKeyIndex($keyType);
32433295
if ($otherIndex === null) {
@@ -3248,7 +3300,7 @@ private function legacyMergeWith(self $otherArray): self
32483300
$optionalKeys[] = $i;
32493301
}
32503302
$otherValueType = $otherArray->valueTypes[$otherIndex];
3251-
$valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $otherValueType);
3303+
$valueTypes[$i] = $mergedValues[spl_object_id($valueTypes[$i]) . ':' . spl_object_id($otherValueType)] ??= TypeCombinator::union($valueTypes[$i], $otherValueType);
32523304
}
32533305

32543306
$optionalKeys = array_values(array_unique($optionalKeys));

‎src/Type/TypeCombinator.php‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,13 +1685,28 @@ private static function optimizeConstantArrays(array $types): array
16851685
public static function countConstantArrayValueTypes(array $types): int
16861686
{
16871687
$constantArrayValuesCount = 0;
1688+
// A type shared by several positions (array shape keys written
1689+
// through a union offset all hold the same value type) is counted
1690+
// once per position, but traversed only once. The entries keep the
1691+
// visited types alive so that their object ids are not reused.
1692+
$countsByType = [];
16881693
foreach ($types as $type) {
1689-
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount): Type {
1694+
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount, &$countsByType): Type {
1695+
$id = spl_object_id($type);
1696+
if (array_key_exists($id, $countsByType)) {
1697+
$constantArrayValuesCount += $countsByType[$id][1];
1698+
return $type;
1699+
}
1700+
1701+
$countBefore = $constantArrayValuesCount;
16901702
if ($type instanceof ConstantArrayType) {
16911703
$constantArrayValuesCount += count($type->getValueTypes());
16921704
}
16931705

1694-
return $traverse($type);
1706+
$result = $traverse($type);
1707+
$countsByType[$id] = [$type, $constantArrayValuesCount - $countBefore];
1708+
1709+
return $result;
16951710
});
16961711
}
16971712
return $constantArrayValuesCount;
@@ -1713,15 +1728,23 @@ private static function getConstantScalarValuesByKey(ConstantArrayType $array):
17131728

17141729
$values = [];
17151730
$valueTypes = $array->getValueTypes();
1731+
// keys written through a union offset share one value type - inspect it once
1732+
$singleValues = [];
17161733
foreach ($array->getKeyTypes() as $i => $keyType) {
1717-
if (!$valueTypes[$i]->isConstantScalarValue()->yes()) {
1718-
continue;
1734+
$id = spl_object_id($valueTypes[$i]);
1735+
if (!array_key_exists($id, $singleValues)) {
1736+
$singleValues[$id] = null;
1737+
if ($valueTypes[$i]->isConstantScalarValue()->yes()) {
1738+
$constantScalarValues = $valueTypes[$i]->getConstantScalarValues();
1739+
if (count($constantScalarValues) === 1) {
1740+
$singleValues[$id] = [$constantScalarValues[0]];
1741+
}
1742+
}
17191743
}
1720-
$constantScalarValues = $valueTypes[$i]->getConstantScalarValues();
1721-
if (count($constantScalarValues) !== 1) {
1744+
if ($singleValues[$id] === null) {
17221745
continue;
17231746
}
1724-
$values[$keyType->getValue()] = $constantScalarValues[0];
1747+
$values[$keyType->getValue()] = $singleValues[$id][0];
17251748
}
17261749

17271750
return $values;

‎tests/PHPStan/Analyser/nsrt/array_values.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ public function constantArrayType(): void
3838
assertType("list{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers));
3939
}
4040

41+
/**
42+
* @param array{a: 1, b?: 2, c: 3, d?: 4, e?: 5, f: 6} $shape
43+
*/
44+
public function optionalKeysBetweenRequiredKeys(array $shape): void
45+
{
46+
assertType('list{0: 1, 1: 2|3, 2: 3|4|5|6, 3?: 4|5|6, 4?: 5|6, 5?: 6}', array_values($shape));
47+
}
48+
4149
/**
4250
* @param array<string, non-empty-array<string, int>> $a
4351
*/

0 commit comments

Comments
 (0)