Skip to content

Commit 85be15c

Browse files
committed
Optimize intersection of domain and key types
1 parent 586288d commit 85be15c

File tree

1 file changed

+100
-70
lines changed

1 file changed

+100
-70
lines changed

lib/elixir/lib/module/types/descr.ex

Lines changed: 100 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,10 +2284,46 @@ defmodule Module.Types.Descr do
22842284
{acc, dynamic?}
22852285
end
22862286

2287+
# TODO: Rename this to tuple_tag_to_type
22872288
defp tag_to_type(:open), do: term_or_optional()
22882289
defp tag_to_type(:closed), do: not_set()
2289-
defp tag_to_type({:closed, domain}), do: Map.get(domain, domain_key(:atom), not_set())
2290-
defp tag_to_type({:open, domain}), do: Map.get(domain, domain_key(:atom), term_or_optional())
2290+
2291+
# Rename this to map_key_tag_to_type
2292+
defp map_key_tag_to_type(:open), do: term_or_optional()
2293+
defp map_key_tag_to_type(:closed), do: not_set()
2294+
defp map_key_tag_to_type({:closed, domain}), do: Map.get(domain, domain_key(:atom), not_set())
2295+
2296+
defp map_key_tag_to_type({:open, domain}),
2297+
do: Map.get(domain, domain_key(:atom), term_or_optional())
2298+
2299+
# Helpers for domain key validation
2300+
# TODO: Merge this and the next clause into one
2301+
defp split_domain_key_pairs(pairs) do
2302+
Enum.split_with(pairs, fn
2303+
{domain_key(_), _} -> false
2304+
_ -> true
2305+
end)
2306+
end
2307+
2308+
defp validate_domain_keys(pairs) do
2309+
# Check if domain keys are valid and don't overlap
2310+
domains = Enum.map(pairs, fn {domain_key(domain), _} -> domain end)
2311+
2312+
if length(domains) != length(Enum.uniq(domains)) do
2313+
raise ArgumentError, "Domain key types should not overlap"
2314+
end
2315+
2316+
# Check that all domain keys are valid
2317+
invalid_domains = Enum.reject(domains, &(&1 in @domain_key_types))
2318+
2319+
if invalid_domains != [] do
2320+
raise ArgumentError,
2321+
"Invalid domain key types: #{inspect(invalid_domains)}. " <>
2322+
"Valid types are: #{inspect(@domain_key_types)}"
2323+
end
2324+
2325+
Enum.map(pairs, fn {key, type} -> {key, if_set(type)} end)
2326+
end
22912327

22922328
defguardp is_optional_static(map)
22932329
when is_map(map) and is_map_key(map, :optional)
@@ -2474,8 +2510,8 @@ defmodule Module.Types.Descr do
24742510
# For a closed map with domains intersected with an open map with domains:
24752511
# 1. The result is closed (more restrictive)
24762512
# 2. We need to check each domain in the open map against the closed map
2477-
default1 = tag_to_type(tag1)
2478-
default2 = tag_to_type(tag2)
2513+
default1 = map_key_tag_to_type(tag1)
2514+
default2 = map_key_tag_to_type(tag2)
24792515

24802516
# Compute the new domain
24812517
tag = map_domain_intersection(tag1, tag2)
@@ -2486,27 +2522,10 @@ defmodule Module.Types.Descr do
24862522
# 3. If key is only in map2, compute non empty intersection with atom1
24872523
# We do that by computing intersection on all key labels in both map1 and map2,
24882524
# using default values when a key is not present.
2489-
keys1_set = :sets.from_list(Map.keys(map1), version: 2)
2490-
keys2_set = :sets.from_list(Map.keys(map2), version: 2)
2491-
2492-
# Combine all unique keys using :sets.union
2493-
all_keys_set = :sets.union(keys1_set, keys2_set)
2494-
all_keys = :sets.to_list(all_keys_set)
2495-
2496-
new_fields =
2497-
for key <- all_keys do
2498-
in_map1? = Map.has_key?(map1, key)
2499-
in_map2? = Map.has_key?(map2, key)
2500-
2501-
cond do
2502-
in_map1? and in_map2? -> {key, non_empty_intersection!(map1[key], map2[key])}
2503-
in_map1? -> {key, non_empty_intersection!(map1[key], default2)}
2504-
in_map2? -> {key, non_empty_intersection!(default1, map2[key])}
2505-
end
2506-
end
2507-
|> :maps.from_list()
2508-
2509-
{tag, new_fields}
2525+
{tag,
2526+
symmetrical_merge(map1, default1, map2, default2, fn _key, v1, v2 ->
2527+
non_empty_intersection!(v1, v2)
2528+
end)}
25102529
end
25112530

25122531
# Compute the intersection of two tags or tag-domain pairs.
@@ -2516,8 +2535,8 @@ defmodule Module.Types.Descr do
25162535
defp map_domain_intersection(tag, :open), do: tag
25172536

25182537
defp map_domain_intersection({tag1, domains1}, {tag2, domains2}) do
2519-
default1 = tag_to_type(tag1)
2520-
default2 = tag_to_type(tag2)
2538+
default1 = map_key_tag_to_type(tag1)
2539+
default2 = map_key_tag_to_type(tag2)
25212540

25222541
new_domains =
25232542
for domain_key <- @domain_key_types, reduce: %{} do
@@ -2568,7 +2587,7 @@ defmodule Module.Types.Descr do
25682587
{:open, fields2, []}, dnf1 when map_size(fields2) == 1 ->
25692588
Enum.reduce(dnf1, [], fn {tag1, fields1, negs1}, acc ->
25702589
{key, value, _rest} = :maps.next(:maps.iterator(fields2))
2571-
t_diff = difference(Map.get(fields1, key, tag_to_type(tag1)), value)
2590+
t_diff = difference(Map.get(fields1, key, map_key_tag_to_type(tag1)), value)
25722591

25732592
if empty?(t_diff) do
25742593
acc
@@ -2641,7 +2660,11 @@ defmodule Module.Types.Descr do
26412660
# Optimization: if the key does not exist in the map, avoid building
26422661
# if_set/not_set pairs and return the popped value directly.
26432662
defp map_fetch_static(%{map: [{tag, fields, []}]}, key) when not is_map_key(fields, key) do
2644-
tag_to_type(tag) |> pop_optional_static()
2663+
case tag do
2664+
:open -> {true, term()}
2665+
:closed -> {true, none()}
2666+
other -> map_key_tag_to_type(other) |> pop_optional_static()
2667+
end
26452668
end
26462669

26472670
# Takes a map dnf and returns the union of types it can take for a given key.
@@ -2655,7 +2678,7 @@ defmodule Module.Types.Descr do
26552678

26562679
# Optimization: if there are no negatives and the key does not exist, return the default one.
26572680
{tag, %{}, []}, acc ->
2658-
tag_to_type(tag) |> union(acc)
2681+
map_key_tag_to_type(tag) |> union(acc)
26592682

26602683
{tag, fields, negs}, acc ->
26612684
{fst, snd} = map_pop_key(tag, fields, key)
@@ -3026,7 +3049,7 @@ defmodule Module.Types.Descr do
30263049

30273050
key_type, acc ->
30283051
# Note: we could stop if we reach term()_or_optional()
3029-
Map.get(domains, domain_key(key_type), tag_to_type(tag)) |> union(acc)
3052+
Map.get(domains, domain_key(key_type), map_key_tag_to_type(tag)) |> union(acc)
30303053
end)
30313054
end
30323055

@@ -3115,15 +3138,15 @@ defmodule Module.Types.Descr do
31153138
dnf
31163139
|> Enum.reduce(none(), fn
31173140
{tag, _fields, []}, acc when is_atom(tag) ->
3118-
tag_to_type(tag) |> union(acc)
3141+
map_key_tag_to_type(tag) |> union(acc)
31193142

31203143
# Optimization: if there are no negatives and domains exists, return its value
31213144
{{_tag, %{domain_key(^key_domain) => value}}, _fields, []}, acc ->
31223145
value |> union(acc)
31233146

31243147
# Optimization: if there are no negatives and the key does not exist, return the default type.
31253148
{{tag, %{}}, _fields, []}, acc ->
3126-
tag_to_type(tag) |> union(acc)
3149+
map_key_tag_to_type(tag) |> union(acc)
31273150

31283151
{tag, fields, negs}, acc ->
31293152
{fst, snd} = map_pop_domain(tag, fields, key_domain)
@@ -3252,8 +3275,8 @@ defmodule Module.Types.Descr do
32523275

32533276
defp map_empty?(tag, fields, [{neg_tag, neg_fields} | negs]) do
32543277
if map_check_domain_keys(tag, neg_tag) do
3255-
atom_default = tag_to_type(tag)
3256-
neg_atom_default = tag_to_type(neg_tag)
3278+
atom_default = map_key_tag_to_type(tag)
3279+
neg_atom_default = map_key_tag_to_type(neg_tag)
32573280

32583281
(Enum.all?(neg_fields, fn {neg_key, neg_type} ->
32593282
cond do
@@ -3418,7 +3441,7 @@ defmodule Module.Types.Descr do
34183441
defp map_pop_key(tag, fields, key) do
34193442
case :maps.take(key, fields) do
34203443
{value, fields} -> {value, %{map: map_new(tag, fields)}}
3421-
:error -> {tag_to_type(tag), %{map: map_new(tag, fields)}}
3444+
:error -> {map_key_tag_to_type(tag), %{map: map_new(tag, fields)}}
34223445
end
34233446
end
34243447

@@ -3428,12 +3451,12 @@ defmodule Module.Types.Descr do
34283451
defp map_pop_domain({tag, domains}, fields, domain_key) do
34293452
case :maps.take(domain_key(domain_key), domains) do
34303453
{value, domains} -> {value, %{map: map_new(tag, fields, domains)}}
3431-
:error -> {tag_to_type(tag), %{map: map_new(tag, fields, domains)}}
3454+
:error -> {map_key_tag_to_type(tag), %{map: map_new(tag, fields, domains)}}
34323455
end
34333456
end
34343457

34353458
defp map_pop_domain(tag, fields, _domain_key),
3436-
do: {tag_to_type(tag), %{map: map_new(tag, fields)}}
3459+
do: {map_key_tag_to_type(tag), %{map: map_new(tag, fields)}}
34373460

34383461
defp map_split_negative(negs, key) do
34393462
Enum.reduce_while(negs, [], fn
@@ -4532,9 +4555,9 @@ defmodule Module.Types.Descr do
45324555

45334556
## Map helpers
45344557

4558+
# Erlang maps:merge_with/3 has to preserve the order in combiner.
4559+
# We don't care about the order, so we have a faster implementation.
45354560
defp symmetrical_merge(left, right, fun) do
4536-
# Erlang maps:merge_with/3 has to preserve the order in combiner.
4537-
# We don't care about the order, so we have a faster implementation.
45384561
if map_size(left) > map_size(right) do
45394562
iterator_merge(:maps.next(:maps.iterator(right)), left, fun)
45404563
else
@@ -4554,9 +4577,44 @@ defmodule Module.Types.Descr do
45544577

45554578
defp iterator_merge(:none, map, _fun), do: map
45564579

4580+
# Perform a symmetrical merge with default values
4581+
defp symmetrical_merge(left, left_default, right, right_default, fun) do
4582+
iterator = :maps.next(:maps.iterator(left))
4583+
iterator_merge_left(iterator, left_default, right, right_default, %{}, fun)
4584+
end
4585+
4586+
defp iterator_merge_left({key, v1, iterator}, v1_default, map, v2_default, acc, fun) do
4587+
value =
4588+
case map do
4589+
%{^key => v2} -> fun.(key, v1, v2)
4590+
%{} -> fun.(key, v1, v2_default)
4591+
end
4592+
4593+
acc = Map.put(acc, key, value)
4594+
iterator_merge_left(:maps.next(iterator), v1_default, map, v2_default, acc, fun)
4595+
end
4596+
4597+
defp iterator_merge_left(:none, v1_default, map, _v2_default, acc, fun) do
4598+
iterator_merge_right(:maps.next(:maps.iterator(map)), v1_default, acc, fun)
4599+
end
4600+
4601+
defp iterator_merge_right({key, v2, iterator}, v1_default, acc, fun) do
4602+
acc =
4603+
case acc do
4604+
%{^key => _} -> acc
4605+
%{} -> Map.put(acc, key, fun.(key, v1_default, v2))
4606+
end
4607+
4608+
iterator_merge_right(:maps.next(iterator), v1_default, acc, fun)
4609+
end
4610+
4611+
defp iterator_merge_right(:none, _v1_default, acc, _fun) do
4612+
acc
4613+
end
4614+
4615+
# Erlang maps:intersect_with/3 has to preserve the order in combiner.
4616+
# We don't care about the order, so we have a faster implementation.
45574617
defp symmetrical_intersection(left, right, fun) do
4558-
# Erlang maps:intersect_with/3 has to preserve the order in combiner.
4559-
# We don't care about the order, so we have a faster implementation.
45604618
if map_size(left) > map_size(right) do
45614619
iterator_intersection(:maps.next(:maps.iterator(right)), left, [], fun)
45624620
else
@@ -4610,32 +4668,4 @@ defmodule Module.Types.Descr do
46104668
defp non_empty_map_or([head | tail], fun) do
46114669
Enum.reduce(tail, fun.(head), &{:or, [], [&2, fun.(&1)]})
46124670
end
4613-
4614-
# Helpers for domain key validation
4615-
defp split_domain_key_pairs(pairs) do
4616-
Enum.split_with(pairs, fn
4617-
{domain_key(_), _} -> false
4618-
_ -> true
4619-
end)
4620-
end
4621-
4622-
defp validate_domain_keys(pairs) do
4623-
# Check if domain keys are valid and don't overlap
4624-
domains = Enum.map(pairs, fn {domain_key(domain), _} -> domain end)
4625-
4626-
if length(domains) != length(Enum.uniq(domains)) do
4627-
raise ArgumentError, "Domain key types should not overlap"
4628-
end
4629-
4630-
# Check that all domain keys are valid
4631-
invalid_domains = Enum.reject(domains, &(&1 in @domain_key_types))
4632-
4633-
if invalid_domains != [] do
4634-
raise ArgumentError,
4635-
"Invalid domain key types: #{inspect(invalid_domains)}. " <>
4636-
"Valid types are: #{inspect(@domain_key_types)}"
4637-
end
4638-
4639-
Enum.map(pairs, fn {key, type} -> {key, if_set(type)} end)
4640-
end
46414671
end

0 commit comments

Comments
 (0)