@@ -1307,8 +1307,115 @@ defmodule Module.Types.Descr do
1307
1307
1308
1308
defp map_only? ( descr ) , do: empty? ( Map . delete ( descr , :map ) )
1309
1309
1310
- # Union is list concatenation
1311
- defp map_union ( dnf1 , dnf2 ) , do: dnf1 ++ ( dnf2 -- dnf1 )
1310
+ defp map_union ( dnf1 , dnf2 ) do
1311
+ # Union is just concatenation, but we rely on some optimization strategies to
1312
+ # avoid the list to grow when possible
1313
+
1314
+ # first pass trying to identify patterns where two maps can be fused as one
1315
+ with [ map1 ] <- dnf1 ,
1316
+ [ map2 ] <- dnf2 ,
1317
+ optimized when optimized != nil <- maybe_optimize_map_union ( map1 , map2 ) do
1318
+ [ optimized ]
1319
+ else
1320
+ # otherwise we just concatenate and remove structural duplicates
1321
+ _ -> dnf1 ++ ( dnf2 -- dnf1 )
1322
+ end
1323
+ end
1324
+
1325
+ defp maybe_optimize_map_union ( { tag1 , pos1 , [ ] } = map1 , { tag2 , pos2 , [ ] } = map2 ) do
1326
+ case map_union_optimization_strategy ( tag1 , pos1 , tag2 , pos2 ) do
1327
+ :all_equal ->
1328
+ map1
1329
+
1330
+ :any_map ->
1331
+ { :open , % { } , [ ] }
1332
+
1333
+ { :one_key_difference , key , v1 , v2 } ->
1334
+ new_pos = Map . put ( pos1 , key , union ( v1 , v2 ) )
1335
+ { tag1 , new_pos , [ ] }
1336
+
1337
+ :left_subtype_of_right ->
1338
+ map2
1339
+
1340
+ :right_subtype_of_left ->
1341
+ map1
1342
+
1343
+ nil ->
1344
+ nil
1345
+ end
1346
+ end
1347
+
1348
+ defp maybe_optimize_map_union ( _ , _ ) , do: nil
1349
+
1350
+ defp map_union_optimization_strategy ( tag1 , pos1 , tag2 , pos2 )
1351
+ defp map_union_optimization_strategy ( tag , pos , tag , pos ) , do: :all_equal
1352
+ defp map_union_optimization_strategy ( :open , empty , _ , _ ) when empty == % { } , do: :any_map
1353
+ defp map_union_optimization_strategy ( _ , _ , :open , empty ) when empty == % { } , do: :any_map
1354
+
1355
+ defp map_union_optimization_strategy ( tag , pos1 , tag , pos2 )
1356
+ when map_size ( pos1 ) == map_size ( pos2 ) do
1357
+ :maps . iterator ( pos1 )
1358
+ |> :maps . next ( )
1359
+ |> do_map_union_optimization_strategy ( pos2 , :all_equal )
1360
+ end
1361
+
1362
+ defp map_union_optimization_strategy ( :open , pos1 , _ , pos2 )
1363
+ when map_size ( pos1 ) <= map_size ( pos2 ) do
1364
+ :maps . iterator ( pos1 )
1365
+ |> :maps . next ( )
1366
+ |> do_map_union_optimization_strategy ( pos2 , :right_subtype_of_left )
1367
+ end
1368
+
1369
+ defp map_union_optimization_strategy ( _ , pos1 , :open , pos2 )
1370
+ when map_size ( pos1 ) >= map_size ( pos2 ) do
1371
+ :maps . iterator ( pos2 )
1372
+ |> :maps . next ( )
1373
+ |> do_map_union_optimization_strategy ( pos1 , :right_subtype_of_left )
1374
+ |> case do
1375
+ :right_subtype_of_left -> :left_subtype_of_right
1376
+ nil -> nil
1377
+ end
1378
+ end
1379
+
1380
+ defp map_union_optimization_strategy ( _ , _ , _ , _ ) , do: nil
1381
+
1382
+ defp do_map_union_optimization_strategy ( :none , _ , status ) , do: status
1383
+
1384
+ defp do_map_union_optimization_strategy ( { key , v1 , iterator } , pos2 , status ) do
1385
+ with % { ^ key => v2 } <- pos2 ,
1386
+ next_status when next_status != nil <- map_union_next_strategy ( key , v1 , v2 , status ) do
1387
+ do_map_union_optimization_strategy ( :maps . next ( iterator ) , pos2 , next_status )
1388
+ else
1389
+ _ -> nil
1390
+ end
1391
+ end
1392
+
1393
+ defp map_union_next_strategy ( key , v1 , v2 , status )
1394
+
1395
+ # structurally equal values do not impact the ongoing strategy
1396
+ defp map_union_next_strategy ( _key , same , same , status ) , do: status
1397
+
1398
+ defp map_union_next_strategy ( key , v1 , v2 , :all_equal ) do
1399
+ if key != :__struct__ , do: { :one_key_difference , key , v1 , v2 }
1400
+ end
1401
+
1402
+ defp map_union_next_strategy ( _key , v1 , v2 , { :one_key_difference , _ , d1 , d2 } ) do
1403
+ # we have at least two key differences now, we switch strategy
1404
+ # if both are subtypes in one direction, keep checking
1405
+ cond do
1406
+ subtype? ( d1 , d2 ) and subtype? ( v1 , v2 ) -> :left_subtype_of_right
1407
+ subtype? ( d2 , d1 ) and subtype? ( v2 , v1 ) -> :right_subtype_of_left
1408
+ true -> nil
1409
+ end
1410
+ end
1411
+
1412
+ defp map_union_next_strategy ( _key , v1 , v2 , :left_subtype_of_right ) do
1413
+ if subtype? ( v1 , v2 ) , do: :left_subtype_of_right
1414
+ end
1415
+
1416
+ defp map_union_next_strategy ( _key , v1 , v2 , :right_subtype_of_left ) do
1417
+ if subtype? ( v2 , v1 ) , do: :right_subtype_of_left
1418
+ end
1312
1419
1313
1420
# Given two unions of maps, intersects each pair of maps.
1314
1421
defp map_intersection ( dnf1 , dnf2 ) do
@@ -1790,49 +1897,21 @@ defmodule Module.Types.Descr do
1790
1897
1791
1898
defp map_non_negated_fuse ( maps ) do
1792
1899
Enum . reduce ( maps , [ ] , fn map , acc ->
1793
- case Enum . split_while ( acc , & non_fusible_maps? ( map , & 1 ) ) do
1794
- { _ , [ ] } ->
1795
- [ map | acc ]
1796
-
1797
- { others , [ match | rest ] } ->
1798
- fused = map_non_negated_fuse_pair ( map , match )
1799
- others ++ [ fused | rest ]
1800
- end
1900
+ fuse_with_first_fusible ( map , acc )
1801
1901
end )
1802
1902
end
1803
1903
1804
- # Two maps are fusible if they differ in at most one element.
1805
- # Given they are of the same size, the side you traverse is not important.
1806
- defp non_fusible_maps? ( { _ , fields1 , [ ] } , { _ , fields2 , [ ] } ) do
1807
- not fusible_maps? ( Map . to_list ( fields1 ) , fields2 , 0 )
1808
- end
1809
-
1810
- defp fusible_maps? ( [ { :__struct__ , value } | rest ] , fields , count ) do
1811
- case Map . fetch! ( fields , :__struct__ ) do
1812
- ^ value -> fusible_maps? ( rest , fields , count )
1813
- _ -> false
1814
- end
1815
- end
1904
+ defp fuse_with_first_fusible ( map , [ ] ) , do: [ map ]
1816
1905
1817
- defp fusible_maps? ( [ { key , value } | rest ] , fields , count ) do
1818
- case Map . fetch! ( fields , key ) do
1819
- ^ value -> fusible_maps? ( rest , fields , count )
1820
- _ when count == 1 -> false
1821
- _ when count == 0 -> fusible_maps? ( rest , fields , count + 1 )
1906
+ defp fuse_with_first_fusible ( map , [ candidate | rest ] ) do
1907
+ if fused = maybe_optimize_map_union ( map , candidate ) do
1908
+ # we found a fusible candidate, we're done
1909
+ [ fused | rest ]
1910
+ else
1911
+ [ candidate | fuse_with_first_fusible ( map , rest ) ]
1822
1912
end
1823
1913
end
1824
1914
1825
- defp fusible_maps? ( [ ] , _fields , _count ) , do: true
1826
-
1827
- defp map_non_negated_fuse_pair ( { tag , fields1 , [ ] } , { _ , fields2 , [ ] } ) do
1828
- fields =
1829
- symmetrical_merge ( fields1 , fields2 , fn _k , v1 , v2 ->
1830
- if v1 == v2 , do: v1 , else: union ( v1 , v2 )
1831
- end )
1832
-
1833
- { tag , fields , [ ] }
1834
- end
1835
-
1836
1915
# If all fields are the same except one, we can optimize map difference.
1837
1916
defp map_all_but_one? ( tag1 , fields1 , tag2 , fields2 ) do
1838
1917
keys1 = Map . keys ( fields1 )
0 commit comments