From db93ca9ba77fe54def8f270c51e26217165f29b3 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 8 Oct 2024 16:03:08 +0200 Subject: [PATCH 1/6] functional insert_at/delete_at --- lib/elixir/lib/module/types/descr.ex | 248 +++++++++++++++++- .../test/elixir/module/types/descr_test.exs | 161 +++++++++++- 2 files changed, 404 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index c5fcc9a6493..340c9f68efe 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 [] -> [] @@ -1362,6 +1369,61 @@ defmodule Module.Types.Descr do end end + # defp tuple_values(dnf) do + # Enum.reduce(dnf, none(), fn {tag, elements, negs}, acc -> + # union(tuple_values(tag, elements, negs, []), acc) + # end) + # end + + # defp tuple_values(:open, _, [], _), do: term() + # defp tuple_values(:closed, elements, [], acc) do + # Enum.reduce(elements, none(), &union/2) + # |> union(Enum.reduce(acc, none(), fn { + # end + + # defp tuple_values( + + # defp tuple_values(tag, elements, negs, acc) do + # end + + # # Transforms a tuple dnf into a union of tuple literals. + # defp tuple_normalize(dnf) do + # Enum.flat_map(dnf, fn {tag, elements, negs} -> tuple_normalize([{tag, elements}], negs) end) + # end + + # defp tuple_normalize(acc, [], acc), do: acc + + # defp tuple_normalize(acc, [{neg_tag, neg_elements} | negs]) do + # end + + # defp expand_negation(tag, elements, neg_tag, neg_elements) do + # n = length(elements) + # m = length(neg_elements) + + # # Scenarios where the difference is guaranteed to be empty: + # # 1. When removing larger tuples from a fixed-size positive tuple + # # 2. When removing smaller tuples from larger tuples + # if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do + # [] + # else + # expand_elements(acc, tag, elements, neg_tag, neg_elements) ++ + # expand_compatibility(n, m, tag, elements, neg_tag, neg_elements) + # end + # end + + # Expand the negation of a tuple, considering the terms where one element + # in the positive tuple is not in the negative tuple. + # defp expand_elements(acc, tag, elements, neg_tag, [neg_type | neg_elements]) do + # {ty, elements} = List.pop_at(elements, 0, term()) + # diff = difference(ty, neg_type) + + # if empty?(diff) do + # expand_elements([ty acc, tag, elements, neg_tag, neg_elements) + # else + # expand_elements([ty | acc], tag, elements, neg_tag, neg_elements) + # end + # end + # Check if a tuple represented in DNF is empty defp tuple_empty?(dnf) do Enum.all?(dnf, fn {tag, pos, negs} -> tuple_empty?(tag, pos, negs) end) @@ -1393,6 +1455,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 +1595,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 +1604,183 @@ 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? -> :outofrange + 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? -> + dynamic_result = tuple_delete_static(dynamic, index) + static_result = tuple_delete_static(static, index) + + union(dynamic(dynamic_result), static_result) + + is_proper_tuple? -> + :outofrange + + 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? -> :outofrange + true -> :badtuple + end + + {dynamic, static} when static == @none -> + if descr_key?(dynamic, :tuple) do + dynamic(insert_element(dynamic, index, type)) + else + :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? -> + dynamic_result = insert_element(dynamic, index, type) + static_result = insert_element(static, index, type) + + union(dynamic(dynamic_result), static_result) + + is_proper_tuple? -> + :outofrange + + true -> + :badtuple + end + end + end + + 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() + + def tuple_values(:term), do: :badtuple + + # Get the union of all the possible values in a tuple + # There are several cases possible: + # 1. The tuple is open, then tuple_value returns term() + # 2. The tuple is closed and has a fixed number of elements, of which we compute the union + # The difficulty is that it may be possible to have a positive open + # tuple which is negated by another open tuple e.g. {integer(), ...} and not {term(), term(), ...} + # which makes this type of fixed size two. + # The other is that even with a closed tuple, negations can remove some possible types, e.g. + # {number()} and not {integer()} means those tuples can only contain floats. + + def tuple_open?(%{tuple: dnf}) do + Enum.any?(dnf, fn {tag, _, _} -> tag == :open end) + end + + def tuple_values(descr) do + case :maps.take(:dynamic, descr) do + :error -> + if descr_key?(descr, :tuple) and tuple_only?(descr) do + tuple_values_static(descr) + else + :badtuple + end + + {dynamic, static} -> + if descr_key?(dynamic, :tuple) and tuple_only?(static) do + dynamic_result = tuple_values_static(dynamic) + static_result = tuple_values_static(static) + union(dynamic(dynamic_result), static_result) + else + :badtuple + end + end + end + + defp tuple_values_static(%{tuple: dnf}) do + Enum.reduce_while(dnf, none(), fn + {:open, _elements, _negs}, _acc -> + {:halt, term()} + + {:closed, elements, _negs}, acc -> + {:cont, union(acc, Enum.reduce(elements, none(), &union(&2, &1)))} + end) + end + + defp tuple_values_static(empty) when empty == @none, 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 1cf3417503a..e97a597b638 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,165 @@ defmodule Module.Types.DescrTest do {true, union(atom(), dynamic())} end + test "tuple_delete_at" do + # 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 with an out-of-bounds index + assert tuple_delete_at(tuple([integer(), atom()]), 3) == :outofrange + + # Test deleting with a negative index + assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex + + # Test deleting from an empty tuple + assert tuple_delete_at(empty_tuple(), 0) == :outofrange + + # Test deleting from a non-tuple type + assert tuple_delete_at(integer(), 0) == :badtuple + + # Test deleting from a term() + assert tuple_delete_at(term(), 0) == :badtuple + + # 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()])))) + end + + test "tuple_insert_at" do + # 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 with an out-of-bounds index + assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :outofrange + + # Out-of-bounds in a union + assert union(tuple([integer(), atom()]), tuple([float()])) + |> tuple_insert_at(2, boolean()) == :outofrange + + # Inserting with a negative index + assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex + + # Inserting into a non-tuple type + assert tuple_insert_at(integer(), 0, boolean()) == :badtuple + + # Inserting into a term() + assert tuple_insert_at(term(), 0, boolean()) == :badtuple + + # Test inserting into an intersection of tuples + assert tuple([integer(), atom()]) + |> tuple_insert_at(1, float()) + |> equal?(tuple([integer(), float(), 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()])) + ) + ) + end + + test "tuple_values" do + # Test with a closed tuple + assert tuple_values(tuple([integer(), atom(), boolean()])) == + union(integer(), union(atom(), boolean())) + + # Test with an empty tuple + assert tuple_values(empty_tuple()) == none() + + # Test with a dynamic tuple + assert tuple_values(dynamic(tuple([integer(), atom()]))) == + dynamic(union(integer(), atom())) + + # Test with an open tuple + assert tuple_values(open_tuple([integer(), atom()])) == term() + + # Test with a union of tuples + assert tuple_values(union(tuple([integer(), atom()]), tuple([float(), binary()]))) == + union(integer(), union(float(), union(atom(), binary()))) + + # Test with an intersection of tuples + assert tuple_values(intersection(tuple([term(), atom()]), tuple([integer(), term()]))) == + union(integer(), atom()) + + # Test with a difference of tuples + assert tuple_values( + difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + ) == union(integer(), union(atom(), boolean())) + + # Test with a complex union involving dynamic + assert tuple_values(union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()])))) + |> equal?(union(union(integer(), atom()), dynamic(union(float(), binary())))) + + # Test with a non-tuple type + assert tuple_values(integer()) == :badtuple + + # Test with term() + assert tuple_values(term()) == :badtuple + end + test "map_fetch" do assert map_fetch(term(), :a) == :badmap assert map_fetch(union(open_map(), integer()), :a) == :badmap From 54025ceb6a9debe5013c5b7596eac30fa6695851 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Tue, 8 Oct 2024 23:11:44 +0200 Subject: [PATCH 2/6] smallfix --- lib/elixir/lib/module/types/descr.ex | 61 +++---------------- .../test/elixir/module/types/descr_test.exs | 47 ++------------ 2 files changed, 13 insertions(+), 95 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 340c9f68efe..7c8d7f08e2b 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1616,7 +1616,7 @@ defmodule Module.Types.Descr do cond do is_proper_tuple? and is_proper_size? -> tuple_delete_static(descr, index) - is_proper_tuple? -> :outofrange + is_proper_tuple? -> :badrange true -> :badtuple end @@ -1632,7 +1632,7 @@ defmodule Module.Types.Descr do union(dynamic(dynamic_result), static_result) is_proper_tuple? -> - :outofrange + :badrange true -> :badtuple @@ -1653,6 +1653,11 @@ defmodule Module.Types.Descr do def tuple_insert_at(_, _, _), do: :badindex + # insert_at({...}, 1, integer()) + # {...} not a subtype of {term(), ...} + # insert_at(dynamic({...}), 1, integer()) + # dynamic({term(), integer(), ...}) + defp tuple_insert_static_value(descr, index, type) do case :maps.take(:dynamic, descr) do :error -> @@ -1664,7 +1669,7 @@ defmodule Module.Types.Descr do cond do is_proper_tuple? and is_proper_size? -> insert_element(descr, index, type) - is_proper_tuple? -> :outofrange + is_proper_tuple? -> :badrange true -> :badtuple end @@ -1689,7 +1694,7 @@ defmodule Module.Types.Descr do union(dynamic(dynamic_result), static_result) is_proper_tuple? -> - :outofrange + :badrange true -> :badtuple @@ -1733,54 +1738,6 @@ defmodule Module.Types.Descr do # If there is no map part to this static type, there is nothing to delete. defp tuple_delete_static(_type, _key), do: none() - def tuple_values(:term), do: :badtuple - - # Get the union of all the possible values in a tuple - # There are several cases possible: - # 1. The tuple is open, then tuple_value returns term() - # 2. The tuple is closed and has a fixed number of elements, of which we compute the union - # The difficulty is that it may be possible to have a positive open - # tuple which is negated by another open tuple e.g. {integer(), ...} and not {term(), term(), ...} - # which makes this type of fixed size two. - # The other is that even with a closed tuple, negations can remove some possible types, e.g. - # {number()} and not {integer()} means those tuples can only contain floats. - - def tuple_open?(%{tuple: dnf}) do - Enum.any?(dnf, fn {tag, _, _} -> tag == :open end) - end - - def tuple_values(descr) do - case :maps.take(:dynamic, descr) do - :error -> - if descr_key?(descr, :tuple) and tuple_only?(descr) do - tuple_values_static(descr) - else - :badtuple - end - - {dynamic, static} -> - if descr_key?(dynamic, :tuple) and tuple_only?(static) do - dynamic_result = tuple_values_static(dynamic) - static_result = tuple_values_static(static) - union(dynamic(dynamic_result), static_result) - else - :badtuple - end - end - end - - defp tuple_values_static(%{tuple: dnf}) do - Enum.reduce_while(dnf, none(), fn - {:open, _elements, _negs}, _acc -> - {:halt, term()} - - {:closed, elements, _negs}, acc -> - {:cont, union(acc, Enum.reduce(elements, none(), &union(&2, &1)))} - end) - end - - defp tuple_values_static(empty) when empty == @none, 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 e97a597b638..a0c9ff4bc28 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -496,13 +496,13 @@ defmodule Module.Types.DescrTest do union(tuple([integer()]), tuple([float()])) # Test deleting with an out-of-bounds index - assert tuple_delete_at(tuple([integer(), atom()]), 3) == :outofrange + assert tuple_delete_at(tuple([integer(), atom()]), 3) == :badrange # Test deleting with a negative index assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex # Test deleting from an empty tuple - assert tuple_delete_at(empty_tuple(), 0) == :outofrange + assert tuple_delete_at(empty_tuple(), 0) == :badrange # Test deleting from a non-tuple type assert tuple_delete_at(integer(), 0) == :badtuple @@ -558,11 +558,11 @@ defmodule Module.Types.DescrTest do union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) # Test inserting with an out-of-bounds index - assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :outofrange + assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :badrange # Out-of-bounds in a union assert union(tuple([integer(), atom()]), tuple([float()])) - |> tuple_insert_at(2, boolean()) == :outofrange + |> tuple_insert_at(2, boolean()) == :badrange # Inserting with a negative index assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex @@ -594,45 +594,6 @@ defmodule Module.Types.DescrTest do ) end - test "tuple_values" do - # Test with a closed tuple - assert tuple_values(tuple([integer(), atom(), boolean()])) == - union(integer(), union(atom(), boolean())) - - # Test with an empty tuple - assert tuple_values(empty_tuple()) == none() - - # Test with a dynamic tuple - assert tuple_values(dynamic(tuple([integer(), atom()]))) == - dynamic(union(integer(), atom())) - - # Test with an open tuple - assert tuple_values(open_tuple([integer(), atom()])) == term() - - # Test with a union of tuples - assert tuple_values(union(tuple([integer(), atom()]), tuple([float(), binary()]))) == - union(integer(), union(float(), union(atom(), binary()))) - - # Test with an intersection of tuples - assert tuple_values(intersection(tuple([term(), atom()]), tuple([integer(), term()]))) == - union(integer(), atom()) - - # Test with a difference of tuples - assert tuple_values( - difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) - ) == union(integer(), union(atom(), boolean())) - - # Test with a complex union involving dynamic - assert tuple_values(union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()])))) - |> equal?(union(union(integer(), atom()), dynamic(union(float(), binary())))) - - # Test with a non-tuple type - assert tuple_values(integer()) == :badtuple - - # Test with term() - assert tuple_values(term()) == :badtuple - end - test "map_fetch" do assert map_fetch(term(), :a) == :badmap assert map_fetch(union(open_map(), integer()), :a) == :badmap From 1c72f9b57b123330d5a6c89d3af872a2b395b268 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Thu, 17 Oct 2024 18:30:51 +0200 Subject: [PATCH 3/6] refactor: Improve tuple manipulation functions --- lib/elixir/lib/module/types/descr.ex | 26 +++++++++---------- .../test/elixir/module/types/descr_test.exs | 16 ++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 7c8d7f08e2b..bcf33acba6c 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1626,7 +1626,11 @@ defmodule Module.Types.Descr do cond do is_proper_tuple? and is_proper_size? -> - dynamic_result = tuple_delete_static(dynamic, index) + # Prune for dynamic values make the intersection succeed + dynamic_result = + intersection(dynamic, tuple_of_size_at_least(index)) + |> tuple_delete_static(index) + static_result = tuple_delete_static(static, index) union(dynamic(dynamic_result), static_result) @@ -1653,11 +1657,6 @@ defmodule Module.Types.Descr do def tuple_insert_at(_, _, _), do: :badindex - # insert_at({...}, 1, integer()) - # {...} not a subtype of {term(), ...} - # insert_at(dynamic({...}), 1, integer()) - # dynamic({term(), integer(), ...}) - defp tuple_insert_static_value(descr, index, type) do case :maps.take(:dynamic, descr) do :error -> @@ -1673,13 +1672,6 @@ defmodule Module.Types.Descr do true -> :badtuple end - {dynamic, static} when static == @none -> - if descr_key?(dynamic, :tuple) do - dynamic(insert_element(dynamic, index, type)) - else - :badtuple - end - {dynamic, static} -> is_proper_tuple? = descr_key?(dynamic, :tuple) and tuple_only?(static) @@ -1688,7 +1680,11 @@ defmodule Module.Types.Descr do cond do is_proper_tuple? and is_proper_size? -> - dynamic_result = insert_element(dynamic, 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) + static_result = insert_element(static, index, type) union(dynamic(dynamic_result), static_result) @@ -1702,6 +1698,8 @@ defmodule Module.Types.Descr do 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} -> diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index a0c9ff4bc28..af70fa877e4 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -523,6 +523,16 @@ defmodule Module.Types.DescrTest do assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) |> tuple_delete_at(1) |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) + + assert tuple_delete_at(open_tuple([term()]), 0) == tuple() + + # 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 @@ -592,6 +602,12 @@ defmodule Module.Types.DescrTest do 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 From 7762e8788b97c5a5b67ef688aab6ac79d0f18b3f Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Thu, 17 Oct 2024 18:39:01 +0200 Subject: [PATCH 4/6] remove comment --- lib/elixir/lib/module/types/descr.ex | 55 ---------------------------- 1 file changed, 55 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index bcf33acba6c..2a8e9fd7c48 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1369,61 +1369,6 @@ defmodule Module.Types.Descr do end end - # defp tuple_values(dnf) do - # Enum.reduce(dnf, none(), fn {tag, elements, negs}, acc -> - # union(tuple_values(tag, elements, negs, []), acc) - # end) - # end - - # defp tuple_values(:open, _, [], _), do: term() - # defp tuple_values(:closed, elements, [], acc) do - # Enum.reduce(elements, none(), &union/2) - # |> union(Enum.reduce(acc, none(), fn { - # end - - # defp tuple_values( - - # defp tuple_values(tag, elements, negs, acc) do - # end - - # # Transforms a tuple dnf into a union of tuple literals. - # defp tuple_normalize(dnf) do - # Enum.flat_map(dnf, fn {tag, elements, negs} -> tuple_normalize([{tag, elements}], negs) end) - # end - - # defp tuple_normalize(acc, [], acc), do: acc - - # defp tuple_normalize(acc, [{neg_tag, neg_elements} | negs]) do - # end - - # defp expand_negation(tag, elements, neg_tag, neg_elements) do - # n = length(elements) - # m = length(neg_elements) - - # # Scenarios where the difference is guaranteed to be empty: - # # 1. When removing larger tuples from a fixed-size positive tuple - # # 2. When removing smaller tuples from larger tuples - # if (tag == :closed and n < m) or (neg_tag == :closed and n > m) do - # [] - # else - # expand_elements(acc, tag, elements, neg_tag, neg_elements) ++ - # expand_compatibility(n, m, tag, elements, neg_tag, neg_elements) - # end - # end - - # Expand the negation of a tuple, considering the terms where one element - # in the positive tuple is not in the negative tuple. - # defp expand_elements(acc, tag, elements, neg_tag, [neg_type | neg_elements]) do - # {ty, elements} = List.pop_at(elements, 0, term()) - # diff = difference(ty, neg_type) - - # if empty?(diff) do - # expand_elements([ty acc, tag, elements, neg_tag, neg_elements) - # else - # expand_elements([ty | acc], tag, elements, neg_tag, neg_elements) - # end - # end - # Check if a tuple represented in DNF is empty defp tuple_empty?(dnf) do Enum.all?(dnf, fn {tag, pos, negs} -> tuple_empty?(tag, pos, negs) end) From 5072bbc994b70d2981d4e4078414b63b5478a124 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Thu, 17 Oct 2024 18:46:28 +0200 Subject: [PATCH 5/6] clean up tests --- .../test/elixir/module/types/descr_test.exs | 53 ++++++------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index af70fa877e4..b9abf95e738 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -475,6 +475,12 @@ defmodule Module.Types.DescrTest do 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()]) @@ -495,21 +501,6 @@ defmodule Module.Types.DescrTest do assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) == union(tuple([integer()]), tuple([float()])) - # Test deleting with an out-of-bounds index - assert tuple_delete_at(tuple([integer(), atom()]), 3) == :badrange - - # Test deleting with a negative index - assert tuple_delete_at(tuple([integer(), atom()]), -1) == :badindex - - # Test deleting from an empty tuple - assert tuple_delete_at(empty_tuple(), 0) == :badrange - - # Test deleting from a non-tuple type - assert tuple_delete_at(integer(), 0) == :badtuple - - # Test deleting from a term() - assert tuple_delete_at(term(), 0) == :badtuple - # Test deleting from an intersection of tuples assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) |> tuple_delete_at(1) == tuple([integer()]) @@ -524,8 +515,6 @@ defmodule Module.Types.DescrTest do |> tuple_delete_at(1) |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) - assert tuple_delete_at(open_tuple([term()]), 0) == tuple() - # 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()) @@ -536,6 +525,15 @@ defmodule Module.Types.DescrTest do 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()]) @@ -567,27 +565,6 @@ defmodule Module.Types.DescrTest do assert tuple_insert_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) - # Test inserting with an out-of-bounds index - assert tuple_insert_at(tuple([integer(), atom()]), 3, boolean()) == :badrange - - # Out-of-bounds in a union - assert union(tuple([integer(), atom()]), tuple([float()])) - |> tuple_insert_at(2, boolean()) == :badrange - - # Inserting with a negative index - assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex - - # Inserting into a non-tuple type - assert tuple_insert_at(integer(), 0, boolean()) == :badtuple - - # Inserting into a term() - assert tuple_insert_at(term(), 0, boolean()) == :badtuple - - # Test inserting into an intersection of tuples - assert tuple([integer(), atom()]) - |> tuple_insert_at(1, float()) - |> equal?(tuple([integer(), float(), atom()])) - # Test inserting into a difference of tuples assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) |> tuple_insert_at(1, float()) From 9e5b58050362d18abaafc5987b390e74f50a58ad Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Thu, 17 Oct 2024 18:51:42 +0200 Subject: [PATCH 6/6] smallfix --- lib/elixir/lib/module/types/descr.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2a8e9fd7c48..5a39b97792a 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1571,15 +1571,15 @@ defmodule Module.Types.Descr do 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) - static_result = tuple_delete_static(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 @@ -1625,15 +1625,15 @@ defmodule Module.Types.Descr do 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) - static_result = insert_element(static, 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