@@ -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 /**
0 commit comments