diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 16eac25cade..37e32b6b0a7 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -3394,9 +3394,22 @@ defmodule Module.Types.Descr do {tag, fields, negs}, acc -> {value, bdd} = map_pop_key_bdd(tag, fields, key) - negs - |> map_split_negative_key(key, value, bdd) - |> Enum.reduce(acc, fn {value, _}, acc -> union(value, acc) end) + case map_split_negative_pairs_key(negs, key) do + :empty -> + acc + + negative -> + value = + if map_pair_projection_keeps_full_fst?(negative, bdd) do + value + else + negs + |> map_split_negative_key(key, value, bdd) + |> Enum.reduce(none(), fn {value, _}, acc -> union(value, acc) end) + end + + union(value, acc) + end end) catch :open -> {true, term()} @@ -3405,6 +3418,39 @@ defmodule Module.Types.Descr do pop_optional_static(value) end + defp map_split_negative_pairs_key(negs, key) do + Enum.reduce_while(negs, [], fn + bdd_leaf(:open, empty), _acc when is_fields_empty(empty) -> + {:halt, :empty} + + bdd_leaf(tag, fields), neg_acc -> + {:cont, [map_pop_key_bdd(tag, fields, key) | neg_acc]} + end) + end + + # Projection shortcuts for the pair-shaped map split below. These are + # existential checks: if at least one remaining-map sample avoids all negative + # remaining maps, the full key-value side survives; dually, if at least one + # key-value sample avoids all negative key values, the full map-shape side + # survives. If neither shortcut applies, we fall back to the regular split. + defp map_pair_projection_keeps_full_fst?(negative, bdd) do + neg_bdd = + Enum.reduce(negative, :bdd_bot, fn {_neg_value, neg_bdd}, acc -> + map_union(neg_bdd, acc) + end) + + not map_empty?(map_difference(bdd, neg_bdd)) + end + + defp map_pair_projection_keeps_full_snd?(negative, value) do + neg_values = + Enum.reduce(negative, none(), fn {neg_value, _neg_bdd}, acc -> + union(neg_value, acc) + end) + + not empty?(difference(value, neg_values)) + end + defp map_split_negative_key(negs, key, value, bdd) do map_split_negative(negs, value, bdd, fn neg_tag, neg_fields -> case fields_take(key, neg_fields) do @@ -3861,10 +3907,35 @@ defmodule Module.Types.Descr do {tag, fields, negs}, {value, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) - pairs = map_split_negative_key(negs, key, fst, snd) - {maybe_union(value, fn -> Enum.reduce(pairs, none(), &union(elem(&1, 0), &2)) end), - Enum.reduce(pairs, bdd, &map_union(elem(&1, 1), &2))} + case map_split_negative_pairs_key(negs, key) do + :empty -> + {value, bdd} + + negative -> + keep_fst? = + value == nil or map_pair_projection_keeps_full_fst?(negative, snd) + + keep_snd? = map_pair_projection_keeps_full_snd?(negative, fst) + + pairs = + if keep_fst? and keep_snd?, + do: [], + else: map_split_negative_key(negs, key, fst, snd) + + {maybe_union(value, fn -> + if keep_fst? do + fst + else + Enum.reduce(pairs, none(), &union(elem(&1, 0), &2)) + end + end), + if keep_snd? do + map_union(bdd, snd) + else + Enum.reduce(pairs, bdd, &map_union(elem(&1, 1), &2)) + end} + end end) if bdd == :bdd_bot do @@ -4195,13 +4266,12 @@ defmodule Module.Types.Descr do map_domain_tag_to_type(tag, domain_key) |> union(acc) {tag_or_domains, fields, negs}, acc -> - {_found, value, bdd} = map_pop_domain_bdd(tag_or_domains, fields, domain_key) - - negs - |> map_split_negative(value, bdd, fn neg_tag, neg_fields -> - map_pop_domain_bdd(neg_tag, neg_fields, domain_key) - end) - |> Enum.reduce(acc, fn {value, _}, acc -> union(value, acc) end) + if init_map_line_empty?(tag_or_domains, fields, negs) do + acc + else + {_found, value, _bdd} = map_pop_domain_bdd(tag_or_domains, fields, domain_key) + union(value, acc) + end end) end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index f14c5056995..4c28fae59a5 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -24,6 +24,18 @@ defmodule Module.Types.DescrTest do defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type)) defp map_with_default(descr), do: open_map([{to_domain_keys(:term), descr}]) + defp projected_negative_map(size) do + Enum.reduce(1..size, open_map(k: open_map(), x: term()), fn index, acc -> + difference( + acc, + open_map([ + {:k, open_map([{:"value#{index}", integer()}])}, + {:"field#{index}", integer()} + ]) + ) + end) + end + describe "union" do test "bitmap" do assert union(integer(), float()) == union(float(), integer()) @@ -2071,6 +2083,11 @@ defmodule Module.Types.DescrTest do |> map_fetch_key(:a) == {false, integer()} end + # Times out without a projection-only map_fetch_key path + test "map_fetch_key with projected negative maps" do + assert map_fetch_key(projected_negative_map(100), :k) == {false, open_map()} + end + test "map_fetch_key with dynamic" do assert map_fetch_key(dynamic(), :a) == {true, dynamic()} assert map_fetch_key(union(dynamic(), integer()), :a) == :badmap @@ -2224,6 +2241,55 @@ defmodule Module.Types.DescrTest do map = closed_map([{:a, atom([:a])}, {:__struct__, term()}, {domain_key(:atom), pid()}]) {:ok, term} = map_get(map, atom() |> difference(atom([:a]))) assert equal?(term, term()) + + base = open_map([{domain_key(:atom), term()}]) + bad = open_map(a: if_set(negation(integer()))) + map = negation(union(negation(base), bad)) + + assert equal?(map, open_map(a: integer())) + + {:ok, type} = map_get(map, atom()) + assert equal?(type, term()) + + {:ok, type} = map_get(map, atom([:a])) + assert equal?(type, integer()) + + map = closed_map([{:a, term()}, {domain_key(:atom), integer()}]) + + {:ok, type} = map_get(map, atom()) + assert equal?(type, term()) + + {:ok, type} = map_get(map, atom([:a])) + assert equal?(type, term()) + + {:ok, type} = map_get(map, difference(atom(), atom([:a]))) + assert equal?(type, integer()) + + map = + closed_map([{:a, term()}, {domain_key(:atom), integer()}]) + |> difference(open_map(a: negation(pid()))) + + {:ok, type} = map_get(map, atom()) + assert equal?(type, union(integer(), pid())) + + {:ok, type} = map_get(map, atom([:a])) + assert equal?(type, pid()) + + {:ok, type} = map_get(map, difference(atom(), atom([:a]))) + assert equal?(type, integer()) + + map = + closed_map([{:a, term()}, {:b, binary()}, {domain_key(:atom), integer()}]) + |> difference(open_map(a: negation(pid()))) + + {:ok, type} = map_get(map, atom()) + assert equal?(type, union(union(integer(), pid()), binary())) + + {:ok, type} = map_get(map, atom([:a, :b])) + assert equal?(type, union(pid(), binary())) + + {:ok, type} = map_get(map, difference(atom(), atom([:a, :b]))) + assert equal?(type, integer()) end test "with lists" do @@ -2252,6 +2318,11 @@ defmodule Module.Types.DescrTest do assert map_get(map, list(integer())) == {:ok, atom([:empty, :non_empty])} end + + # Times out without a projection-only map_get path + test "with projected negative maps" do + assert map_get(projected_negative_map(100), atom([:k])) == {:ok, open_map()} + end end describe "map_update" do @@ -2337,6 +2408,12 @@ defmodule Module.Types.DescrTest do |> map_update(atom([:b]), integer(), true, true) == {none(), none(), []} end + # Times out without a projection-aware map_update path + test "with projected negative maps" do + assert map_update(projected_negative_map(100), atom([:k]), binary()) == + {open_map(), open_map(k: binary(), x: term()), []} + end + test "with non-empty open maps does not call the callback with none from absent branches" do # This is a test of the map_update_fun/5 with forced?: false parameter. # We check that it does not call its typed_fun argument with `none()` @@ -2802,6 +2879,31 @@ defmodule Module.Types.DescrTest do ]) )} end + + # Times out without proper map_put + test "with projected negative maps" do + map = projected_negative_map(100) + + assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())} + + map = difference(open_map(k: integer(), x: term()), open_map(k: integer(), a: integer())) + + {:ok, type} = map_put(map, atom([:k]), binary()) + + assert equal?( + type, + difference(open_map(k: binary(), x: term()), open_map(k: binary(), a: integer())) + ) + end + + test "with projected negative maps and no popped value projection" do + # map_put/3 passes nil as the popped value accumulator because it only needs the map side. + map = + projected_negative_map(100) + |> difference(open_map(k: atom(), x: term())) + + assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())} + end end describe "disjoint" do diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs index 106a5ecf8c7..52522425729 100644 --- a/lib/elixir/test/elixir/module/types/map_test.exs +++ b/lib/elixir/test/elixir/module/types/map_test.exs @@ -833,6 +833,22 @@ defmodule Module.Types.MapTest do assert typeerror!([x = []], Map.put(x, :key, :value)) =~ "incompatible types given to Map.put/3" end + + test "errors with dynamic key and value" do + assert typeerror!([key, value], Map.put(1, key, value)) |> strip_ansi() =~ """ + incompatible types given to Map.put/3: + + Map.put(1, key, value) + + given types: + + integer(), dynamic(), dynamic() + + but expected one of: + + map(), term(), term() + """ + end end describe "Map.put_new_lazy/3" do