diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index c5fcc9a649..5a39b97792 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -59,13 +59,18 @@ defmodule Module.Types.Descr do def non_empty_list(), do: %{bitmap: @bit_non_empty_list} def open_map(), do: %{map: @map_top} def open_map(pairs), do: map_descr(:open, pairs) - def open_tuple(elements), do: tuple_descr(:open, elements) def pid(), do: %{bitmap: @bit_pid} def port(), do: %{bitmap: @bit_port} def reference(), do: %{bitmap: @bit_reference} def tuple(), do: %{tuple: @tuple_top} + def open_tuple(elements), do: tuple_descr(:open, elements) def tuple(elements), do: tuple_descr(:closed, elements) + # Tuple helper + defp tuple_of_size_at_least(n) when is_integer(n) and n >= 0 do + open_tuple(List.duplicate(term(), n)) + end + @boolset :sets.from_list([true, false], version: 2) def boolean(), do: %{atom: {:union, @boolset}} @@ -827,6 +832,8 @@ defmodule Module.Types.Descr do end end + defp map_put_static_descr(static, _, _) when static == @none, do: @none + # Directly inserts a key of a given type into every positive and negative map defp map_put_static_descr(descr, key, type) do map_delete_static(descr, key) @@ -1319,7 +1326,7 @@ defmodule Module.Types.Descr do defp tuple_to_quoted(dnf) do dnf - |> tuple_normalize() + |> tuple_simplify() |> Enum.map(&tuple_each_to_quoted/1) |> case do [] -> [] @@ -1393,6 +1400,7 @@ defmodule Module.Types.Descr do defp tuple_elements(_, _, _, [], _), do: true defp tuple_elements(acc, tag, elements, [neg_type | neg_elements], negs) do + # Handles the case where {tag, elements} is an open tuple, like {:open, []} {ty, elements} = List.pop_at(elements, 0, term()) diff = difference(ty, neg_type) @@ -1532,8 +1540,8 @@ defmodule Module.Types.Descr do end) end - # Use heuristics to normalize a tuple dnf for pretty printing. - defp tuple_normalize(dnf) do + # Use heuristics to simplify a tuple dnf for pretty printing. + defp tuple_simplify(dnf) do for {tag, elements, negs} <- dnf, not tuple_empty?([{tag, elements, negs}]) do n = length(elements) @@ -1541,6 +1549,138 @@ defmodule Module.Types.Descr do end end + # Same as tuple_delete but checks if the index is out of range. + def tuple_delete_at(:term, _key), do: :badtuple + + def tuple_delete_at(descr, index) when is_integer(index) and index >= 0 do + case :maps.take(:dynamic, descr) do + :error -> + # Note: the empty type is not a valid input + is_proper_tuple? = descr_key?(descr, :tuple) and tuple_only?(descr) + is_proper_size? = subtype?(Map.take(descr, [:tuple]), tuple_of_size_at_least(index + 1)) + + cond do + is_proper_tuple? and is_proper_size? -> tuple_delete_static(descr, index) + is_proper_tuple? -> :badrange + true -> :badtuple + end + + {dynamic, static} -> + is_proper_tuple? = descr_key?(dynamic, :tuple) and tuple_only?(static) + is_proper_size? = subtype?(Map.take(static, [:tuple]), tuple_of_size_at_least(index + 1)) + + cond 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) + + union(dynamic(dynamic_result), static_result) + + # Highlight the case where the issue is an index out of range from the tuple + is_proper_tuple? -> + :badrange + + true -> + :badtuple + end + end + end + + def tuple_delete_at(_, _), do: :badindex + + def tuple_insert_at(:term, _key, _type), do: :badtuple + + def tuple_insert_at(descr, index, type) when is_integer(index) and index >= 0 do + case :maps.take(:dynamic, unfold(type)) do + :error -> tuple_insert_static_value(descr, index, type) + {dynamic, _static} -> dynamic(tuple_insert_static_value(descr, index, dynamic)) + end + end + + def tuple_insert_at(_, _, _), do: :badindex + + defp tuple_insert_static_value(descr, index, type) do + case :maps.take(:dynamic, descr) do + :error -> + # Note: the empty type is not a valid input + is_proper_tuple? = descr_key?(descr, :tuple) and tuple_only?(descr) + + is_proper_size? = + index == 0 or subtype?(Map.take(descr, [:tuple]), tuple_of_size_at_least(index)) + + cond do + is_proper_tuple? and is_proper_size? -> insert_element(descr, index, type) + is_proper_tuple? -> :badrange + true -> :badtuple + end + + {dynamic, static} -> + is_proper_tuple? = descr_key?(dynamic, :tuple) and tuple_only?(static) + + is_proper_size? = + index == 0 or subtype?(Map.take(static, [:tuple]), tuple_of_size_at_least(index)) + + cond do + is_proper_tuple? and is_proper_size? -> + static_result = insert_element(static, index, type) + # Prune for dynamic values that make the intersection succeed + dynamic_result = + intersection(dynamic, tuple_of_size_at_least(index)) + |> insert_element(index, type) + + union(dynamic(dynamic_result), static_result) + + # Highlight the case where the issue is an index out of range from the tuple + is_proper_tuple? -> + :badrange + + true -> + :badtuple + end + end + end + + defp insert_element(descr, _, _) when descr == @none, do: none() + + defp insert_element(descr, index, type) do + Map.update!(descr, :tuple, fn dnf -> + Enum.map(dnf, fn {tag, elements, negs} -> + {tag, List.insert_at(elements, index, type), + Enum.map(negs, fn {neg_tag, neg_elements} -> + {neg_tag, List.insert_at(neg_elements, index, type)} + end)} + end) + end) + end + + # Takes a static map type and removes an index from it. + defp tuple_delete_static(%{tuple: dnf}, index) do + Enum.reduce(dnf, none(), fn + # Optimization: if there are no negatives, we can directly remove the element + {tag, elements, []}, acc -> + union(acc, %{tuple: tuple_new(tag, List.delete_at(elements, index))}) + + {tag, elements, negs}, acc -> + {fst, snd} = tuple_pop_index(tag, elements, index) + + union( + acc, + case tuple_split_negative(negs, index) do + :empty -> none() + negative -> negative |> pair_make_disjoint() |> pair_eliminate_negations_snd(fst, snd) + end + ) + end) + end + + defp tuple_delete_static(:term, key), do: open_map([{key, not_set()}]) + + # If there is no map part to this static type, there is nothing to delete. + defp tuple_delete_static(_type, _key), do: none() + # Remove useless negations, which denote tuples of incompatible sizes. defp tuple_empty_negation?(tag, n, {neg_tag, neg_elements}) do m = length(neg_elements) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 1cf3417503..b9abf95e73 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -187,7 +187,7 @@ defmodule Module.Types.DescrTest do defp empty_tuple(), do: tuple([]) defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n)) - defp tuple_of_size(n) when is_integer(n), do: tuple(List.duplicate(term(), n)) + defp tuple_of_size(n) when is_integer(n) and n >= 0, do: tuple(List.duplicate(term(), n)) test "tuple" do assert empty?(difference(open_tuple([atom()]), open_tuple([term()]))) @@ -474,6 +474,119 @@ defmodule Module.Types.DescrTest do {true, union(atom(), dynamic())} end + test "tuple_delete_at" do + assert tuple_delete_at(tuple([integer(), atom()]), 3) == :badrange + assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex + assert tuple_delete_at(empty_tuple(), 0) == :badrange + assert tuple_delete_at(integer(), 0) == :badtuple + assert tuple_delete_at(term(), 0) == :badtuple + + # Test deleting an element from a closed tuple + assert tuple_delete_at(tuple([integer(), atom(), boolean()]), 1) == + tuple([integer(), boolean()]) + + # Test deleting the last element from a closed tuple + assert tuple_delete_at(tuple([integer(), atom()]), 1) == + tuple([integer()]) + + # Test deleting from an open tuple + assert tuple_delete_at(open_tuple([integer(), atom(), boolean()]), 1) == + open_tuple([integer(), boolean()]) + + # Test deleting from a dynamic tuple + assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 1) == + dynamic(tuple([integer()])) + + # Test deleting from a union of tuples + assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) == + union(tuple([integer()]), tuple([float()])) + + # Test deleting from an intersection of tuples + assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) + |> tuple_delete_at(1) == tuple([integer()]) + + # Test deleting from a difference of tuples + assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + |> tuple_delete_at(1) + |> equal?(tuple([integer(), boolean()])) + + # Test deleting from a complex union involving dynamic + assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + |> tuple_delete_at(1) + |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) + + # Succesfully deleting at position `index` in a tuple means that the dynamic + # values that succeed are intersected with tuples of size at least `index` + assert dynamic(tuple()) |> tuple_delete_at(0) == dynamic(tuple()) + + assert dynamic(union(tuple(), integer())) + |> tuple_delete_at(1) + |> equal?(dynamic(tuple_of_size_at_least(1))) + end + + test "tuple_insert_at" do + assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :badrange + assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex + assert tuple_insert_at(integer(), 0, boolean()) == :badtuple + assert tuple_insert_at(term(), 0, boolean()) == :badtuple + + # Out-of-bounds in a union + assert union(tuple([integer(), atom()]), tuple([float()])) + |> tuple_insert_at(2, boolean()) == :badrange + + # Test inserting into a closed tuple + assert tuple_insert_at(tuple([integer(), atom()]), 1, boolean()) == + tuple([integer(), boolean(), atom()]) + + # Test inserting at the beginning of a tuple + assert tuple_insert_at(tuple([integer(), atom()]), 0, boolean()) == + tuple([boolean(), integer(), atom()]) + + # Test inserting at the end of a tuple + assert tuple_insert_at(tuple([integer(), atom()]), 2, boolean()) == + tuple([integer(), atom(), boolean()]) + + # Test inserting into an empty tuple + assert tuple_insert_at(empty_tuple(), 0, integer()) == tuple([integer()]) + + # Test inserting into an open tuple + assert tuple_insert_at(open_tuple([integer(), atom()]), 1, boolean()) == + open_tuple([integer(), boolean(), atom()]) + + # Test inserting a dynamic type + assert tuple_insert_at(tuple([integer(), atom()]), 1, dynamic()) == + dynamic(tuple([integer(), term(), atom()])) + + # Test inserting into a dynamic tuple + assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == + dynamic(tuple([integer(), boolean(), atom()])) + + # 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()])) + + # Test inserting into a difference of tuples + assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + |> tuple_insert_at(1, float()) + |> equal?(tuple([integer(), float(), atom(), boolean()])) + + # Test inserting into a complex union involving dynamic + assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + |> tuple_insert_at(1, boolean()) + |> equal?( + union( + tuple([integer(), boolean(), atom()]), + dynamic(tuple([float(), boolean(), binary()])) + ) + ) + + # If you succesfully intersect at position index in a type, then the dynamic values + # that succeed are intersected with tuples of size at least index + assert dynamic(union(tuple(), integer())) + |> tuple_insert_at(1, boolean()) + |> equal?(dynamic(open_tuple([term(), boolean()]))) + end + test "map_fetch" do assert map_fetch(term(), :a) == :badmap assert map_fetch(union(open_map(), integer()), :a) == :badmap