@@ -1264,8 +1264,115 @@ defmodule Module.Types.Descr do
1264
1264
1265
1265
defp map_only? ( descr ) , do: empty? ( Map . delete ( descr , :map ) )
1266
1266
1267
- # Union is list concatenation
1268
- defp map_union ( dnf1 , dnf2 ) , do: dnf1 ++ ( dnf2 -- dnf1 )
1267
+ defp map_union ( dnf1 , dnf2 ) do
1268
+ # Union is just concatenation, but we rely on some optimization strategies to
1269
+ # avoid the list to grow when possible
1270
+
1271
+ # first pass trying to identify patterns where two maps can be fused as one
1272
+ with [ map1 ] <- dnf1 ,
1273
+ [ map2 ] <- dnf2 ,
1274
+ optimized when optimized != nil <- maybe_optimize_map_union ( map1 , map2 ) do
1275
+ [ optimized ]
1276
+ else
1277
+ # otherwise we just concatenate and remove structural duplicates
1278
+ _ -> dnf1 ++ ( dnf2 -- dnf1 )
1279
+ end
1280
+ end
1281
+
1282
+ defp maybe_optimize_map_union ( { tag1 , pos1 , [ ] } = map1 , { tag2 , pos2 , [ ] } = map2 ) do
1283
+ case map_union_optimization_strategy ( tag1 , pos1 , tag2 , pos2 ) do
1284
+ :all_equal ->
1285
+ map1
1286
+
1287
+ :any_map ->
1288
+ { :open , % { } , [ ] }
1289
+
1290
+ { :one_key_difference , key , v1 , v2 } ->
1291
+ new_pos = Map . put ( pos1 , key , union ( v1 , v2 ) )
1292
+ { tag1 , new_pos , [ ] }
1293
+
1294
+ :left_subtype_of_right ->
1295
+ map2
1296
+
1297
+ :right_subtype_of_left ->
1298
+ map1
1299
+
1300
+ nil ->
1301
+ nil
1302
+ end
1303
+ end
1304
+
1305
+ defp maybe_optimize_map_union ( _ , _ ) , do: nil
1306
+
1307
+ defp map_union_optimization_strategy ( tag1 , pos1 , tag2 , pos2 )
1308
+ defp map_union_optimization_strategy ( tag , pos , tag , pos ) , do: :all_equal
1309
+ defp map_union_optimization_strategy ( :open , empty , _ , _ ) when empty == % { } , do: :any_map
1310
+ defp map_union_optimization_strategy ( _ , _ , :open , empty ) when empty == % { } , do: :any_map
1311
+
1312
+ defp map_union_optimization_strategy ( tag , pos1 , tag , pos2 )
1313
+ when map_size ( pos1 ) == map_size ( pos2 ) do
1314
+ :maps . iterator ( pos1 )
1315
+ |> :maps . next ( )
1316
+ |> do_map_union_optimization_strategy ( pos2 , :all_equal )
1317
+ end
1318
+
1319
+ defp map_union_optimization_strategy ( :open , pos1 , _ , pos2 )
1320
+ when map_size ( pos1 ) <= map_size ( pos2 ) do
1321
+ :maps . iterator ( pos1 )
1322
+ |> :maps . next ( )
1323
+ |> do_map_union_optimization_strategy ( pos2 , :right_subtype_of_left )
1324
+ end
1325
+
1326
+ defp map_union_optimization_strategy ( _ , pos1 , :open , pos2 )
1327
+ when map_size ( pos1 ) >= map_size ( pos2 ) do
1328
+ :maps . iterator ( pos2 )
1329
+ |> :maps . next ( )
1330
+ |> do_map_union_optimization_strategy ( pos1 , :right_subtype_of_left )
1331
+ |> case do
1332
+ :right_subtype_of_left -> :left_subtype_of_right
1333
+ nil -> nil
1334
+ end
1335
+ end
1336
+
1337
+ defp map_union_optimization_strategy ( _ , _ , _ , _ ) , do: nil
1338
+
1339
+ defp do_map_union_optimization_strategy ( :none , _ , status ) , do: status
1340
+
1341
+ defp do_map_union_optimization_strategy ( { key , v1 , iterator } , pos2 , status ) do
1342
+ with % { ^ key => v2 } <- pos2 ,
1343
+ next_status when next_status != nil <- map_union_next_strategy ( key , v1 , v2 , status ) do
1344
+ do_map_union_optimization_strategy ( :maps . next ( iterator ) , pos2 , next_status )
1345
+ else
1346
+ _ -> nil
1347
+ end
1348
+ end
1349
+
1350
+ defp map_union_next_strategy ( key , v1 , v2 , status )
1351
+
1352
+ # structurally equal values do not impact the ongoing strategy
1353
+ defp map_union_next_strategy ( _key , same , same , status ) , do: status
1354
+
1355
+ defp map_union_next_strategy ( key , v1 , v2 , :all_equal ) do
1356
+ if key != :__struct__ , do: { :one_key_difference , key , v1 , v2 }
1357
+ end
1358
+
1359
+ defp map_union_next_strategy ( _key , v1 , v2 , { :one_key_difference , _ , d1 , d2 } ) do
1360
+ # we have at least two key differences now, we switch strategy
1361
+ # if both are subtypes in one direction, keep checking
1362
+ cond do
1363
+ subtype? ( d1 , d2 ) and subtype? ( v1 , v2 ) -> :left_subtype_of_right
1364
+ subtype? ( d2 , d1 ) and subtype? ( v2 , v1 ) -> :right_subtype_of_left
1365
+ true -> nil
1366
+ end
1367
+ end
1368
+
1369
+ defp map_union_next_strategy ( _key , v1 , v2 , :left_subtype_of_right ) do
1370
+ if subtype? ( v1 , v2 ) , do: :left_subtype_of_right
1371
+ end
1372
+
1373
+ defp map_union_next_strategy ( _key , v1 , v2 , :right_subtype_of_left ) do
1374
+ if subtype? ( v2 , v1 ) , do: :right_subtype_of_left
1375
+ end
1269
1376
1270
1377
# Given two unions of maps, intersects each pair of maps.
1271
1378
defp map_intersection ( dnf1 , dnf2 ) do
@@ -1747,29 +1854,19 @@ defmodule Module.Types.Descr do
1747
1854
1748
1855
defp map_non_negated_fuse ( maps ) do
1749
1856
Enum . reduce ( maps , [ ] , fn map , acc ->
1750
- case Enum . split_while ( acc , & non_fusible_maps? ( map , & 1 ) ) do
1751
- { _ , [ ] } ->
1752
- [ map | acc ]
1753
-
1754
- { others , [ match | rest ] } ->
1755
- fused = map_non_negated_fuse_pair ( map , match )
1756
- others ++ [ fused | rest ]
1757
- end
1857
+ fuse_with_first_fusible ( map , acc )
1758
1858
end )
1759
1859
end
1760
1860
1761
- # Two maps are fusible if they differ in at most one element.
1762
- defp non_fusible_maps? ( { _ , fields1 , [ ] } , { _ , fields2 , [ ] } ) do
1763
- Enum . count_until ( fields1 , fn { key , value } -> Map . fetch! ( fields2 , key ) != value end , 2 ) > 1
1764
- end
1765
-
1766
- defp map_non_negated_fuse_pair ( { tag , fields1 , [ ] } , { _ , fields2 , [ ] } ) do
1767
- fields =
1768
- symmetrical_merge ( fields1 , fields2 , fn _k , v1 , v2 ->
1769
- if v1 == v2 , do: v1 , else: union ( v1 , v2 )
1770
- end )
1861
+ defp fuse_with_first_fusible ( map , [ ] ) , do: [ map ]
1771
1862
1772
- { tag , fields , [ ] }
1863
+ defp fuse_with_first_fusible ( map , [ candidate | rest ] ) do
1864
+ if fused = maybe_optimize_map_union ( map , candidate ) do
1865
+ # we found a fusible candidate, we're done
1866
+ [ fused | rest ]
1867
+ else
1868
+ [ candidate | fuse_with_first_fusible ( map , rest ) ]
1869
+ end
1773
1870
end
1774
1871
1775
1872
# If all fields are the same except one, we can optimize map difference.
0 commit comments