@@ -2178,40 +2178,43 @@ public function isKeysSupersetOf(self $otherArray): bool
21782178 return $ this ->legacyIsKeysSupersetOf ($ otherArray );
21792179 }
21802180
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-
21992181 [$ thisUnsealedKey , $ thisUnsealedValue ] = $ this ->unsealed ;
22002182 [$ otherUnsealedKey , $ otherUnsealedValue ] = $ otherArray ->unsealed ;
22012183 $ thisHasExtras = !($ thisUnsealedKey instanceof NeverType && $ thisUnsealedKey ->isExplicit ());
22022184 $ otherHasExtras = !($ otherUnsealedKey instanceof NeverType && $ otherUnsealedKey ->isExplicit ());
22032185
2204- if ($ hasCommon ) {
2186+ $ otherHasRequiredKeys = false ;
2187+ foreach ($ otherArray ->keyTypes as $ j => $ keyType ) {
2188+ if ($ otherArray ->isOptionalKey ($ j )) {
2189+ continue ;
2190+ }
2191+ $ otherHasRequiredKeys = true ;
2192+ break ;
2193+ }
2194+
2195+ // Sealed empty $other (no keys, no extras): absorbing it is lossless iff $this
2196+ // already accepts []. i.e., all of $this's known keys are optional. Otherwise
2197+ // merge would add [] as a new instance.
2198+ if (!$ otherHasRequiredKeys && !$ otherHasExtras && count ($ otherArray ->keyTypes ) === 0 ) {
2199+ foreach ($ this ->keyTypes as $ i => $ keyType ) {
2200+ if (!$ this ->isOptionalKey ($ i )) {
2201+ return false ;
2202+ }
2203+ }
22052204 return true ;
22062205 }
22072206
2207+ // With real unsealed extras on both sides that can absorb each other's
2208+ // required keys, merging is acceptable regardless of which keys overlap.
22082209 if ($ thisHasExtras && $ otherHasExtras ) {
22092210 return true ;
22102211 }
22112212
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).
2213+ // Asymmetric extras: one side has real extras that can absorb the other's keys.
22142214 if ($ thisHasExtras ) {
2215+ if ($ this ->legacyIsKeysSupersetOf ($ otherArray )) {
2216+ return true ;
2217+ }
22152218 foreach ($ otherArray ->keyTypes as $ j => $ keyType ) {
22162219 if ($ otherArray ->isOptionalKey ($ j )) {
22172220 continue ;
@@ -2227,6 +2230,9 @@ public function isKeysSupersetOf(self $otherArray): bool
22272230 }
22282231
22292232 if ($ otherHasExtras ) {
2233+ if ($ this ->legacyIsKeysSupersetOf ($ otherArray )) {
2234+ return true ;
2235+ }
22302236 foreach ($ this ->keyTypes as $ i => $ keyType ) {
22312237 if ($ this ->isOptionalKey ($ i )) {
22322238 continue ;
@@ -2241,7 +2247,8 @@ public function isKeysSupersetOf(self $otherArray): bool
22412247 return true ;
22422248 }
22432249
2244- return false ;
2250+ // Both sealed: fall back to the legacy key/value shape check.
2251+ return $ this ->legacyIsKeysSupersetOf ($ otherArray );
22452252 }
22462253
22472254 private function legacyIsKeysSupersetOf (self $ otherArray ): bool
@@ -2352,7 +2359,7 @@ public function mergeWith(self $otherArray): self
23522359 $ j = $ otherKeyIndexMap [$ keyValue ];
23532360 $ otherValueType = $ otherArray ->valueTypes [$ j ];
23542361 $ mergedValue = TypeCombinator::union ($ valueType , $ otherValueType );
2355- $ optional = $ this ->isOptionalKey ($ i ) && $ otherArray ->isOptionalKey ($ j );
2362+ $ optional = $ this ->isOptionalKey ($ i ) || $ otherArray ->isOptionalKey ($ j );
23562363
23572364 $ keyTypes [] = $ keyType ;
23582365 $ valueTypes [] = $ mergedValue ;
0 commit comments