Skip to content

Commit 6cacffd

Browse files
committed
union improvement
1 parent 68257a6 commit 6cacffd

6 files changed

Lines changed: 625 additions & 88 deletions

File tree

src/Type/Constant/ConstantArrayType.php

Lines changed: 200 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,18 @@ public function __construct(
131131

132132
$keyTypesCount = count($this->keyTypes);
133133
if ($keyTypesCount === 0) {
134-
$isList = TrinaryLogic::createYes();
134+
if ($unsealed === null) {
135+
$isList = TrinaryLogic::createYes();
136+
} else {
137+
[$unsealedKeyType] = $unsealed;
138+
if ($unsealedKeyType instanceof NeverType && $unsealedKeyType->isExplicit()) {
139+
$isList = TrinaryLogic::createYes();
140+
} elseif ($unsealedKeyType->isInteger()->yes()) {
141+
$isList = TrinaryLogic::createMaybe();
142+
} else {
143+
$isList = TrinaryLogic::createNo();
144+
}
145+
}
135146
}
136147

137148
if ($isList === null) {
@@ -1617,7 +1628,14 @@ public function isIterableAtLeastOnce(): TrinaryLogic
16171628
{
16181629
$keysCount = count($this->keyTypes);
16191630
if ($keysCount === 0) {
1620-
return TrinaryLogic::createNo();
1631+
if ($this->unsealed === null) {
1632+
return TrinaryLogic::createNo();
1633+
}
1634+
[$unsealedKey] = $this->unsealed;
1635+
if ($unsealedKey instanceof NeverType && $unsealedKey->isExplicit()) {
1636+
return TrinaryLogic::createNo();
1637+
}
1638+
return TrinaryLogic::createMaybe();
16211639
}
16221640

16231641
$optionalKeysCount = count($this->optionalKeys);
@@ -2155,6 +2173,78 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
21552173
}
21562174

21572175
public function isKeysSupersetOf(self $otherArray): bool
2176+
{
2177+
if ($this->unsealed === null || $otherArray->unsealed === null) {
2178+
return $this->legacyIsKeysSupersetOf($otherArray);
2179+
}
2180+
2181+
$keyIndexMap = $this->getKeyIndexMap();
2182+
$otherKeyIndexMap = $otherArray->getKeyIndexMap();
2183+
2184+
// Disjoint values at a common key prevent a lossless merge
2185+
$hasCommon = false;
2186+
foreach ($otherKeyIndexMap as $keyValue => $j) {
2187+
if (!array_key_exists($keyValue, $keyIndexMap)) {
2188+
continue;
2189+
}
2190+
$i = $keyIndexMap[$keyValue];
2191+
$valueType = $this->valueTypes[$i];
2192+
$otherValueType = $otherArray->valueTypes[$j];
2193+
if ($valueType->isSuperTypeOf($otherValueType)->no() && $otherValueType->isSuperTypeOf($valueType)->no()) {
2194+
return false;
2195+
}
2196+
$hasCommon = true;
2197+
}
2198+
2199+
[$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
2200+
[$otherUnsealedKey, $otherUnsealedValue] = $otherArray->unsealed;
2201+
$thisHasExtras = !($thisUnsealedKey instanceof NeverType && $thisUnsealedKey->isExplicit());
2202+
$otherHasExtras = !($otherUnsealedKey instanceof NeverType && $otherUnsealedKey->isExplicit());
2203+
2204+
if ($hasCommon) {
2205+
return true;
2206+
}
2207+
2208+
if ($thisHasExtras && $otherHasExtras) {
2209+
return true;
2210+
}
2211+
2212+
// Mixed or both sealed, no common keys — only merge if one side's extras can
2213+
// absorb the other side's required keys (preserves tagged-union otherwise).
2214+
if ($thisHasExtras) {
2215+
foreach ($otherArray->keyTypes as $j => $keyType) {
2216+
if ($otherArray->isOptionalKey($j)) {
2217+
continue;
2218+
}
2219+
if ($thisUnsealedKey->isSuperTypeOf($keyType)->no()) {
2220+
return false;
2221+
}
2222+
if ($thisUnsealedValue->isSuperTypeOf($otherArray->valueTypes[$j])->no()) {
2223+
return false;
2224+
}
2225+
}
2226+
return true;
2227+
}
2228+
2229+
if ($otherHasExtras) {
2230+
foreach ($this->keyTypes as $i => $keyType) {
2231+
if ($this->isOptionalKey($i)) {
2232+
continue;
2233+
}
2234+
if ($otherUnsealedKey->isSuperTypeOf($keyType)->no()) {
2235+
return false;
2236+
}
2237+
if ($otherUnsealedValue->isSuperTypeOf($this->valueTypes[$i])->no()) {
2238+
return false;
2239+
}
2240+
}
2241+
return true;
2242+
}
2243+
2244+
return false;
2245+
}
2246+
2247+
private function legacyIsKeysSupersetOf(self $otherArray): bool
21582248
{
21592249
$keyTypesCount = count($this->keyTypes);
21602250
$otherKeyTypesCount = count($otherArray->keyTypes);
@@ -2208,14 +2298,119 @@ public function isKeysSupersetOf(self $otherArray): bool
22082298
}
22092299
}
22102300

2211-
// todo unsealed
2212-
22132301
return true;
22142302
}
22152303

22162304
public function mergeWith(self $otherArray): self
22172305
{
22182306
// only call this after verifying isKeysSupersetOf, or if losing tagged unions is not an issue
2307+
if ($this->unsealed === null || $otherArray->unsealed === null) {
2308+
return $this->legacyMergeWith($otherArray);
2309+
}
2310+
2311+
[$thisUnsealedKey, $thisUnsealedValue] = $this->unsealed;
2312+
[$otherUnsealedKey, $otherUnsealedValue] = $otherArray->unsealed;
2313+
2314+
$mergedUnsealedKey = TypeCombinator::union($thisUnsealedKey, $otherUnsealedKey);
2315+
$mergedUnsealedValue = TypeCombinator::union($thisUnsealedValue, $otherUnsealedValue);
2316+
2317+
$resultUnsealed = [$mergedUnsealedKey, $mergedUnsealedValue];
2318+
$resultHasExtras = !($mergedUnsealedKey instanceof NeverType && $mergedUnsealedKey->isExplicit());
2319+
2320+
$absorbIntoExtras = static function (Type $keyType, Type $valueType) use (&$mergedUnsealedKey, &$mergedUnsealedValue): void {
2321+
$mergedUnsealedKey = TypeCombinator::union($mergedUnsealedKey, $keyType);
2322+
$mergedUnsealedValue = TypeCombinator::union($mergedUnsealedValue, $valueType);
2323+
};
2324+
2325+
$canAbsorb = static function (Type $sideUnsealedKey, Type $sideUnsealedValue, Type $keyType, Type $valueType): bool {
2326+
if ($sideUnsealedKey instanceof NeverType && $sideUnsealedKey->isExplicit()) {
2327+
return false;
2328+
}
2329+
if ($sideUnsealedKey->isSuperTypeOf($keyType)->no()) {
2330+
return false;
2331+
}
2332+
if ($sideUnsealedValue->isSuperTypeOf($valueType)->no()) {
2333+
return false;
2334+
}
2335+
return true;
2336+
};
2337+
2338+
$keyTypes = [];
2339+
$valueTypes = [];
2340+
$optionalKeys = [];
2341+
$nextAutoIndexes = [0];
2342+
2343+
$otherKeyIndexMap = $otherArray->getKeyIndexMap();
2344+
$processed = [];
2345+
2346+
foreach ($this->keyTypes as $i => $keyType) {
2347+
$keyValue = $keyType->getValue();
2348+
$processed[$keyValue] = true;
2349+
$valueType = $this->valueTypes[$i];
2350+
2351+
if (array_key_exists($keyValue, $otherKeyIndexMap)) {
2352+
$j = $otherKeyIndexMap[$keyValue];
2353+
$otherValueType = $otherArray->valueTypes[$j];
2354+
$mergedValue = TypeCombinator::union($valueType, $otherValueType);
2355+
$optional = $this->isOptionalKey($i) && $otherArray->isOptionalKey($j);
2356+
2357+
$keyTypes[] = $keyType;
2358+
$valueTypes[] = $mergedValue;
2359+
if ($optional) {
2360+
$optionalKeys[] = count($keyTypes) - 1;
2361+
}
2362+
continue;
2363+
}
2364+
2365+
if ($canAbsorb($otherUnsealedKey, $otherUnsealedValue, $keyType, $valueType)) {
2366+
$absorbIntoExtras($keyType, $valueType);
2367+
continue;
2368+
}
2369+
2370+
$keyTypes[] = $keyType;
2371+
$valueTypes[] = $valueType;
2372+
$optionalKeys[] = count($keyTypes) - 1;
2373+
}
2374+
2375+
foreach ($otherArray->keyTypes as $j => $keyType) {
2376+
$keyValue = $keyType->getValue();
2377+
if (array_key_exists($keyValue, $processed)) {
2378+
continue;
2379+
}
2380+
$valueType = $otherArray->valueTypes[$j];
2381+
2382+
if ($canAbsorb($thisUnsealedKey, $thisUnsealedValue, $keyType, $valueType)) {
2383+
$absorbIntoExtras($keyType, $valueType);
2384+
continue;
2385+
}
2386+
2387+
$keyTypes[] = $keyType;
2388+
$valueTypes[] = $valueType;
2389+
$optionalKeys[] = count($keyTypes) - 1;
2390+
}
2391+
2392+
$resultUnsealed = [$mergedUnsealedKey, $mergedUnsealedValue];
2393+
2394+
$nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)));
2395+
sort($nextAutoIndexes);
2396+
2397+
$optionalKeys = array_values(array_unique($optionalKeys));
2398+
2399+
/** @var list<ConstantIntegerType|ConstantStringType> $keyTypes */
2400+
$keyTypes = $keyTypes;
2401+
2402+
return $this->recreate(
2403+
$keyTypes,
2404+
$valueTypes,
2405+
$nextAutoIndexes,
2406+
$optionalKeys,
2407+
$this->isList->and($otherArray->isList),
2408+
$resultUnsealed,
2409+
);
2410+
}
2411+
2412+
private function legacyMergeWith(self $otherArray): self
2413+
{
22192414
$valueTypes = $this->valueTypes;
22202415
$optionalKeys = $this->optionalKeys;
22212416
foreach ($this->keyTypes as $i => $keyType) {
@@ -2236,7 +2431,7 @@ public function mergeWith(self $otherArray): self
22362431
$nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes)));
22372432
sort($nextAutoIndexes);
22382433

2239-
return $this->recreate($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList), $this->unsealed); // todo unsealed
2434+
return $this->recreate($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList), $this->unsealed);
22402435
}
22412436

22422437
/**

src/Type/Constant/ConstantArrayTypeBuilder.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,13 @@ public function getArray(): Type
382382
{
383383
$keyTypesCount = count($this->keyTypes);
384384
if ($keyTypesCount === 0) {
385+
if ($this->unsealed !== null) {
386+
[$unsealedKey, $unsealedValue] = $this->unsealed;
387+
$isExplicitNever = $unsealedKey instanceof NeverType && $unsealedKey->isExplicit();
388+
if (!$isExplicitNever) {
389+
return new ArrayType($unsealedKey, $unsealedValue);
390+
}
391+
}
385392
return new ConstantArrayType([], [], unsealed: $this->unsealed);
386393
}
387394

src/Type/TypeCombinator.php

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use function array_filter;
3232
use function array_key_exists;
3333
use function array_key_first;
34+
use function array_keys;
3435
use function array_merge;
3536
use function array_slice;
3637
use function array_splice;
@@ -917,7 +918,7 @@ private static function processArrayTypes(array $arrayTypes): array
917918
$filledArrays++;
918919
}
919920

920-
if ($generalArrayOccurred || !$isConstantArray) {
921+
if (!$isConstantArray) {
921922
foreach ($arrayType->getArrays() as $type) {
922923
$keyTypesForGeneralArray[] = $type->getIterableKeyType();
923924
$valueTypesForGeneralArray[] = $type->getItemType();
@@ -1240,6 +1241,61 @@ private static function reduceArrays(array $constantArrays, bool $preserveTagged
12401241
}
12411242
}
12421243

1244+
// Second pass: for arrays with definite sealedness, try to merge pairs that
1245+
// don't share any known key (the eligibleCombinations loop above only considers
1246+
// shared-key pairs).
1247+
$indices = array_keys($arraysToProcess);
1248+
$indicesCount = count($indices);
1249+
for ($ii = 0; $ii < $indicesCount - 1; $ii++) {
1250+
$i = $indices[$ii];
1251+
if (!array_key_exists($i, $arraysToProcess)) {
1252+
continue;
1253+
}
1254+
if ($arraysToProcess[$i]->getUnsealedTypes() === null) {
1255+
continue;
1256+
}
1257+
for ($jj = $ii + 1; $jj < $indicesCount; $jj++) {
1258+
$j = $indices[$jj];
1259+
if (!array_key_exists($j, $arraysToProcess)) {
1260+
continue;
1261+
}
1262+
if ($arraysToProcess[$j]->getUnsealedTypes() === null) {
1263+
continue;
1264+
}
1265+
if ($arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])) {
1266+
$arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1267+
unset($arraysToProcess[$i]);
1268+
continue 2;
1269+
}
1270+
if (!$arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])) {
1271+
continue;
1272+
}
1273+
1274+
$arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]);
1275+
unset($arraysToProcess[$j]);
1276+
}
1277+
}
1278+
1279+
// Final pass: if merging left us with a ConstantArrayType that has no known keys
1280+
// but has real unsealed extras, collapse it to a plain ArrayType (mirrors the same
1281+
// logic in ConstantArrayTypeBuilder::getArray — but applies to results produced by
1282+
// ConstantArrayType::mergeWith, which doesn't go through the builder).
1283+
foreach ($arraysToProcess as $idx => $arr) {
1284+
if (count($arr->getKeyTypes()) !== 0) {
1285+
continue;
1286+
}
1287+
$unsealed = $arr->getUnsealedTypes();
1288+
if ($unsealed === null) {
1289+
continue;
1290+
}
1291+
[$unsealedKey, $unsealedValue] = $unsealed;
1292+
if ($unsealedKey instanceof NeverType && $unsealedKey->isExplicit()) {
1293+
continue;
1294+
}
1295+
$newArrays[] = new ArrayType($unsealedKey, $unsealedValue);
1296+
unset($arraysToProcess[$idx]);
1297+
}
1298+
12431299
return array_merge($newArrays, $arraysToProcess);
12441300
}
12451301

@@ -1471,6 +1527,7 @@ public static function intersect(Type ...$types): Type
14711527
&& $types[$j] instanceof NonEmptyArrayType
14721528
&& (count($types[$i]->getKeyTypes()) === 1 || $types[$i]->isList()->yes())
14731529
&& $types[$i]->isOptionalKey(0)
1530+
&& !$types[$i]->isUnsealed()->yes()
14741531
) {
14751532
$types[$i] = $types[$i]->makeOffsetRequired($types[$i]->getKeyTypes()[0]);
14761533
array_splice($types, $j--, 1);
@@ -1483,6 +1540,7 @@ public static function intersect(Type ...$types): Type
14831540
&& $types[$i] instanceof NonEmptyArrayType
14841541
&& (count($types[$j]->getKeyTypes()) === 1 || $types[$j]->isList()->yes())
14851542
&& $types[$j]->isOptionalKey(0)
1543+
&& !$types[$j]->isUnsealed()->yes()
14861544
) {
14871545
$types[$j] = $types[$j]->makeOffsetRequired($types[$j]->getKeyTypes()[0]);
14881546
array_splice($types, $i--, 1);

0 commit comments

Comments
 (0)