diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index e210dc6404..bbef71f22a 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5584,15 +5584,26 @@ defmodule Module.Types.Descr do {optional?, descr} = tuple_fetch_element(elements, index, tag) {optional? or acc_optional?, union(descr, acc_descr)} - {tag, elements, negs}, acc -> + {tag, elements, negs}, {acc_optional?, acc_descr} -> {_, value, bdd} = tuple_take_element(elements, index, tag) - negs - |> tuple_split_negative(index, value, bdd) - |> Enum.reduce(acc, fn {value, _}, {acc_optional?, acc_descr} -> - {optional?, descr} = pop_optional_static(value) - {optional? or acc_optional?, union(descr, acc_descr)} - end) + case tuple_split_negative_pairs_index(negs, index) do + :empty -> + {acc_optional?, acc_descr} + + negative -> + value = + if tuple_pair_projection_keeps_full_fst?(negative, bdd) do + value + else + negs + |> tuple_split_negative(index, value, bdd) + |> Enum.reduce(none(), fn {value, _}, acc -> union(value, acc) end) + end + + {optional?, descr} = pop_optional_static(value) + {optional? or acc_optional?, union(descr, acc_descr)} + end end) catch :open -> {true, term()} @@ -5649,6 +5660,40 @@ defmodule Module.Types.Descr do :empty -> [] end + defp tuple_split_negative_pairs_index(negs, index) do + Enum.reduce_while(negs, [], fn + bdd_leaf(:open, []), _acc -> + {:halt, :empty} + + bdd_leaf(tag, elements), neg_acc -> + {_found?, neg_value, neg_bdd} = tuple_take_element(elements, index, tag) + {:cont, [{neg_value, neg_bdd} | neg_acc]} + end) + end + + # Projection shortcuts for the pair-shaped tuple split below. These are + # existential checks: if at least one remaining-tuple sample avoids all + # negative remaining tuples, the full value side survives; dually, if at least + # one value sample avoids all negative values, the full remaining-tuple side + # survives. If neither shortcut applies, we fall back to the regular split. + defp tuple_pair_projection_keeps_full_fst?(negative, bdd) do + neg_bdd = + Enum.reduce(negative, :bdd_bot, fn {_neg_value, neg_bdd}, acc -> + tuple_union(neg_bdd, acc) + end) + + not tuple_empty?(tuple_difference(bdd, neg_bdd)) + end + + defp tuple_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 tuple_fetch_element([], _, :open), do: {true, term()} defp tuple_fetch_element([], _, :closed), do: {true, none()} defp tuple_fetch_element([h | _], 0, _tag), do: {false, h} @@ -5740,12 +5785,15 @@ defmodule Module.Types.Descr do is_proper_tuple? and is_proper_size? -> static_result = tuple_delete_static(static, index) - # Prune for dynamic values make the intersection succeed - dynamic_result = - intersection(dynamic, tuple_of_size_at_least(index)) - |> tuple_delete_static(index) + # Prune for dynamic values that make the operation succeed. + dynamic_input = intersection(dynamic, tuple_of_size_at_least(index + 1)) - union(dynamic(dynamic_result), static_result) + if empty?(dynamic_input) and empty?(static) do + :badindex + else + dynamic_result = tuple_delete_static(dynamic_input, index) + union(dynamic(dynamic_result), static_result) + end # Highlight the case where the issue is an index out of range from the tuple is_proper_tuple? -> @@ -5759,17 +5807,38 @@ defmodule Module.Types.Descr do def tuple_delete_at(_, _), do: :badindex - # Takes a static map type and removes an index from it. + # Takes a static tuple type and removes an index from it. defp tuple_delete_static(%{tuple: bdd}, index) do - %{ - tuple: - bdd_map(bdd, fn bdd_leaf(tag, elements) -> - bdd_leaf_new(tag, List.delete_at(elements, index)) - end) - } + bdd = + bdd + |> tuple_bdd_to_dnf_with_negations() + |> Enum.reduce(:bdd_bot, fn + {tag, elements, []}, acc -> + {_, _, bdd} = tuple_take_element(elements, index, tag) + tuple_union(bdd, acc) + + {tag, elements, negs}, acc -> + {_, value, bdd} = tuple_take_element(elements, index, tag) + + case tuple_split_negative_pairs_index(negs, index) do + :empty -> + acc + + negative -> + if tuple_pair_projection_keeps_full_snd?(negative, value) do + tuple_union(bdd, acc) + else + negs + |> tuple_split_negative(index, value, bdd) + |> Enum.reduce(acc, fn {_, bdd}, acc -> tuple_union(bdd, acc) end) + end + end + end) + + %{tuple: bdd} end - # If there is no map part to this static type, there is nothing to delete. + # If there is no tuple part to this static type, there is nothing to delete. defp tuple_delete_static(_type, _key), do: none() @doc """ @@ -5810,11 +5879,14 @@ defmodule Module.Types.Descr do static_result = tuple_insert_static(static, index, type) # Prune for dynamic values that make the intersection succeed - dynamic_result = - intersection(dynamic, tuple_of_size_at_least(index)) - |> tuple_insert_static(index, type) + dynamic_input = intersection(dynamic, tuple_of_size_at_least(index)) - union(dynamic(dynamic_result), static_result) + if empty?(dynamic_input) and empty?(static) do + :badindex + else + dynamic_result = tuple_insert_static(dynamic_input, index, type) + union(dynamic(dynamic_result), static_result) + end # Highlight the case where the issue is an index out of range from the tuple is_proper_tuple? -> @@ -5854,14 +5926,50 @@ defmodule Module.Types.Descr do defp tuple_of_size_at_least_static?(descr, index) do case descr do %{tuple: bdd} -> - tuple_bdd_to_dnf_no_negations(bdd) - |> Enum.all?(fn {_, elements} -> length(elements) >= index end) + tuple_bdd_positive_size_at_least?(bdd, index) or + tuple_empty?(tuple_difference(bdd, tuple_new(:open, List.duplicate(term(), index)))) %{} -> true end end + # This is a cheap proof that a tuple type has a size lower bound. + # + # In an intersection of tuples, the size is lower-bounded by each tuple literal + # we encounter. So we can find a lower bound on the size by making sure that all + # paths in the bdd have at least one tuple literal with a size >= to index. + # + # We don't need to check that the elements are not equivalent to none(), since in + # that case the intersection will be equivalent to none() and will disappear. + # + # This misses the case where the lower bound is proven by negations. For instance, + # the type "{...} and not {}" has a size of at least 1. + defp tuple_bdd_positive_size_at_least?(_bdd, 0), do: true + + defp tuple_bdd_positive_size_at_least?(bdd, index), + do: tuple_bdd_positive_size_at_least?(bdd, index, false) + + defp tuple_bdd_positive_size_at_least?(_bdd, _index, true), do: true + defp tuple_bdd_positive_size_at_least?(:bdd_bot, _index, _guaranteed?), do: true + defp tuple_bdd_positive_size_at_least?(:bdd_top, _index, guaranteed?), do: guaranteed? + + defp tuple_bdd_positive_size_at_least?(bdd_leaf(_tag, elements), index, _guaranteed?) do + length(elements) >= index + end + + defp tuple_bdd_positive_size_at_least?( + {_, bdd_leaf(_tag, elements), c, u, d}, + index, + guaranteed? + ) do + literal_guaranteed? = length(elements) >= index + + tuple_bdd_positive_size_at_least?(u, index, guaranteed?) and + tuple_bdd_positive_size_at_least?(c, index, literal_guaranteed?) and + tuple_bdd_positive_size_at_least?(d, index, guaranteed?) + end + ## BDD helpers # Some of our operations rely on eliminating closed tuples/maps, diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index e01160d5b0..53c49536b7 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -36,6 +36,18 @@ defmodule Module.Types.DescrTest do end) end + defp projected_negative_tuple(size) do + Enum.reduce(1..size, open_tuple([open_tuple([]), term()]), fn index, acc -> + difference( + acc, + open_tuple([ + open_tuple([atom([:"value#{index}"])]), + integer() + ]) + ) + end) + end + describe "union" do test "bitmap" do assert union(integer(), float()) == union(float(), integer()) @@ -1719,6 +1731,8 @@ defmodule Module.Types.DescrTest do |> tuple_fetch(2) == {false, integer()} assert tuple_fetch(tuple(), 0) == :badindex + + assert tuple_fetch(projected_negative_tuple(200), 1) == {false, term()} end test "tuple_fetch with dynamic" do @@ -1759,6 +1773,8 @@ defmodule Module.Types.DescrTest do assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 1) == dynamic(tuple([integer()])) + assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 2) == :badindex + # Test deleting from a union of tuples assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) |> equal?(tuple([union(integer(), float())])) @@ -1772,6 +1788,13 @@ defmodule Module.Types.DescrTest do |> tuple_delete_at(1) |> equal?(tuple([integer(), boolean()])) + assert difference( + open_tuple([open_tuple([]), term()]), + open_tuple([open_tuple([atom([:value])]), integer()]) + ) + |> tuple_delete_at(1) + |> equal?(open_tuple([open_tuple([])])) + # Test deleting from a complex union involving dynamic assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) |> tuple_delete_at(1) @@ -1825,10 +1848,19 @@ defmodule Module.Types.DescrTest do assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == dynamic(tuple([integer(), boolean(), atom()])) + assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 3, boolean()) == :badindex + # Test inserting into a union of tuples assert tuple_insert_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) + assert difference(tuple(), empty_tuple()) + |> tuple_insert_at(1, boolean()) + |> equal?(open_tuple([term(), boolean()])) + + inserted = tuple_insert_at(projected_negative_tuple(200), 1, atom([:inserted])) + assert tuple_fetch(inserted, 1) == {false, atom([:inserted])} + # Test inserting into a difference of tuples assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) |> tuple_insert_at(1, float())