Skip to content

Commit 0401e57

Browse files
committed
Decide sealed array shapes with disjoint required keys before comparing values
ConstantArrayType::isSuperTypeOf() walks the keys of $this in order and only reports that the other shape lacks a required key when it reaches that key - after asking the other shape for the offset and comparing the value of every key before it. Two sealed shapes where one has a required key the other lacks share no value, and that is now decided from the key sets first. The answer is the same No; when a value mismatch also comes earlier in key order, the reason is now the sealed-shapes one instead of the offset's. hasOffsetValueType() and getOffsetValueType() also scanned every key with isSuperTypeOf() for a constant offset the key index has no entry for, which can only ever find nothing. A non-compound constant int or string offset missing from the index now skips that scan. A chain of `if ($pN !== null) { $data['kN'] = $pN; }` builds a sealed shape under bleedingEdge, and every if-merge rechecks the growing list of conditional holders guarded by earlier shapes of $data against the branch's new shape (preserveVacuousConditionalExpressions()). With 90 ifs that took 1.8 s in those checks alone; each check is now a few key lookups. Amp-Thread-ID: https://ampcode.com/threads/T-01a1b24f-ec49-7194-bbd2-0f4a43ab9b8f
1 parent a76fdbf commit 0401e57

5 files changed

Lines changed: 571 additions & 55 deletions

File tree

‎src/Type/Constant/ConstantArrayType.php‎

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,17 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
797797
// $this is unsealed with no known keys — fall through to extras/unsealed-part checks below
798798
}
799799

800+
// Two sealed shapes where one has a required key the other lacks share
801+
// no value. The loops below would reach the same answer, but only after
802+
// comparing the value of every key that precedes the missing one.
803+
if (
804+
$thisUnsealedness->no()
805+
&& $typeUnsealedness->no()
806+
&& ($this->hasRequiredKeyMissingIn($type) || $type->hasRequiredKeyMissingIn($this))
807+
) {
808+
return IsSuperTypeOfResult::createNo(lazyReasons: [fn (): string => $this->sealedArrayShapesCannotBeIntersectedReason($type)]);
809+
}
810+
800811
$results = [];
801812
foreach ($this->keyTypes as $i => $keyType) {
802813
$hasOffset = $type->hasOffsetValueType($keyType);
@@ -1228,8 +1239,9 @@ private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic
12281239
}
12291240

12301241
// Constant offsets against constant keys resolve by value: a hit at the
1231-
// indexed slot is verified before use, a miss falls through to the scan
1232-
// (which also covers the unsealed extras below).
1242+
// indexed slot is verified before use, an offset no key's value collides
1243+
// with skips the scan, any other miss falls through to it (the unsealed
1244+
// extras below are checked either way).
12331245
$index = $this->findVerifiedKeyIndex($offsetType);
12341246
if ($index !== null) {
12351247
if ($this->isOptionalKey($index)) {
@@ -1239,7 +1251,7 @@ private function recursiveHasOffsetValueType(Type $offsetType): TrinaryLogic
12391251
}
12401252

12411253
$result = TrinaryLogic::createNo();
1242-
foreach ($this->keyTypes as $i => $keyType) {
1254+
foreach ($this->isKeyIndexMiss($offsetType) ? [] : $this->keyTypes as $i => $keyType) {
12431255
// PHP coerces decimal-integer strings to int when used as array
12441256
// keys ("123" → 123), so a non-constant string offset *could* hit
12451257
// a constant-integer slot. Skip the upgrade when the offset is
@@ -1310,7 +1322,15 @@ public function getOffsetValueType(Type $offsetType): Type
13101322
$matchingValueTypes = [];
13111323
$all = true;
13121324
$maybeAll = true;
1313-
foreach ($this->keyTypes as $i => $keyType) {
1325+
$keyIndexMiss = count($this->keyTypes) > 0 && $this->isKeyIndexMiss($offsetType);
1326+
if ($keyIndexMiss) {
1327+
// What the scan below concludes for an offset no key's value
1328+
// collides with: every key misses it, and a constant offset cannot
1329+
// be a decimal-integer string hitting an integer key.
1330+
$all = false;
1331+
$maybeAll = false;
1332+
}
1333+
foreach ($keyIndexMiss ? [] : $this->keyTypes as $i => $keyType) {
13141334
if ($keyType->isSuperTypeOf($offsetType)->no()) {
13151335
$all = false;
13161336

@@ -3289,6 +3309,38 @@ private function findVerifiedKeyIndex(Type $offsetType): ?int
32893309
return $index;
32903310
}
32913311

3312+
/**
3313+
* Whether a constant integer or string offset (already passed through
3314+
* toArrayKey()) has a value no explicit key's value collides with as an
3315+
* array key - then no explicit key is a supertype of it, so scanning the
3316+
* keys for one is pointless. A CompoundType offset (a template, an
3317+
* intersection) is left to the scan: its isSubTypeOf() may answer maybe.
3318+
*/
3319+
private function isKeyIndexMiss(Type $offsetType): bool
3320+
{
3321+
if ($offsetType instanceof CompoundType || !$offsetType->isConstantScalarValue()->yes()) {
3322+
return false;
3323+
}
3324+
3325+
$values = $offsetType->getConstantScalarValues();
3326+
if (count($values) !== 1 || (!is_int($values[0]) && !is_string($values[0]))) {
3327+
return false;
3328+
}
3329+
3330+
return !array_key_exists($values[0], $this->getKeyIndexMap());
3331+
}
3332+
3333+
private function hasRequiredKeyMissingIn(self $other): bool
3334+
{
3335+
foreach ($this->keyTypes as $i => $keyType) {
3336+
if (!$this->isOptionalKey($i) && $other->isKeyIndexMiss($keyType)) {
3337+
return true;
3338+
}
3339+
}
3340+
3341+
return false;
3342+
}
3343+
32923344
/**
32933345
* @param ConstantIntegerType|ConstantStringType $otherKeyType
32943346
*/

0 commit comments

Comments
 (0)