Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 83 additions & 13 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
102 changes: 102 additions & 0 deletions lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()`
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions lib/elixir/test/elixir/module/types/map_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading