diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 9dc295f87d..d0e8d4262b 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -487,6 +487,34 @@ defmodule Module.Types.Apply do end end + defp do_remote(:erlang, :setelement, [index, tuple, elem], _, expr, stack, context, of_fun) + when is_integer(index) and index < 1 do + {_, context} = of_fun.(tuple, open_tuple([]), expr, stack, context) + {_, context} = of_fun.(elem, term(), expr, stack, context) + remote_error({:negindex, index - 1}, :erlang, :setelement, 3, expr, stack, context) + end + + defp do_remote(:erlang, :setelement, [index, tuple, elem], _, expr, stack, context, of_fun) + when is_integer(index) do + tuple_type = open_tuple(List.duplicate(term(), max(index, 1))) + + {tuple_type, context} = of_fun.(tuple, tuple_type, expr, stack, context) + {elem_type, context} = of_fun.(elem, term(), expr, stack, context) + + case tuple_replace_at(tuple_type, index - 1, elem_type) do + value_type when is_descr(value_type) -> + {return(value_type, [tuple_type, elem_type], stack), context} + + :badtuple -> + args_types = [integer(), tuple_type, elem_type] + remote_error(:erlang, :setelement, args_types, expr, stack, context) + + :badindex -> + error = {:badindex, index, tuple_type} + remote_error(error, :erlang, :setelement, 3, expr, stack, context) + end + end + defp do_remote(:erlang, :delete_element, [index, tuple], _, expr, stack, context, of_fun) when is_integer(index) do tuple_type = open_tuple(List.duplicate(term(), max(index, 1))) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 9ad6f166c3..824242e650 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5853,10 +5853,22 @@ defmodule Module.Types.Descr do :error -> tuple_insert_at_checked(descr, index, type) - {dynamic_type, _static} -> + {dynamic_type, static_type} -> case tuple_insert_at_checked(descr, index, dynamic_type) do - atom when atom in [:badtuple, :badindex] -> atom - result -> dynamic(result) + dynamic_result when is_descr(dynamic_result) -> + dynamic_result = dynamic(dynamic_result) + + if empty?(static_type) do + dynamic_result + else + case tuple_insert_at_checked(descr, index, static_type) do + static_result when is_descr(static_result) -> union(dynamic_result, static_result) + error -> error + end + end + + error -> + error end end end @@ -5925,6 +5937,20 @@ defmodule Module.Types.Descr do end) end + @doc """ + Replace an element in the tuple at the given (0-based) index. + + It returns the same as `tuple_fetch/2`. + """ + def tuple_replace_at(descr, index, type) when is_integer(index) and index >= 0 do + case tuple_delete_at(descr, index) do + descr when is_descr(descr) -> tuple_insert_at(descr, index, type) + error -> error + end + end + + def tuple_replace_at(_, _, _), do: :badindex + defp tuple_of_size_at_least(n) when is_integer(n) and n >= 0 do %{tuple: tuple_new(:open, List.duplicate(term(), n))} end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index d7e3357d14..cfc5b2b8a0 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1844,6 +1844,18 @@ defmodule Module.Types.DescrTest do assert tuple_insert_at(tuple([integer(), atom()]), 1, dynamic()) == dynamic(tuple([integer(), term(), atom()])) + assert tuple_insert_at( + tuple([boolean()]), + 1, + union(dynamic(integer()), atom([:inserted])) + ) + |> equal?( + union( + tuple([boolean(), atom([:inserted])]), + dynamic(tuple([boolean(), integer()])) + ) + ) + # Test inserting into a dynamic tuple assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == dynamic(tuple([integer(), boolean(), atom()])) @@ -1887,6 +1899,98 @@ defmodule Module.Types.DescrTest do assert tuple_insert_at(tuple([atom([:ok])]), 2, dynamic()) == :badindex end + test "tuple_replace_at" do + assert tuple_replace_at(tuple([integer(), atom()]), 2, boolean()) == :badindex + assert tuple_replace_at(tuple([integer(), atom()]), -1, boolean()) == :badindex + assert tuple_replace_at(empty_tuple(), 0, boolean()) == :badindex + assert tuple_replace_at(integer(), 0, boolean()) == :badtuple + assert tuple_replace_at(term(), 0, boolean()) == :badtuple + assert tuple_replace_at(tuple([none()]), 0, boolean()) == :badtuple + + # Out-of-bounds in a union + assert union(tuple([integer(), atom()]), tuple([float()])) + |> tuple_replace_at(1, boolean()) == :badindex + + # Test replacing an element in a closed tuple + assert tuple_replace_at(tuple([integer(), atom(), boolean()]), 1, float()) == + tuple([integer(), float(), boolean()]) + + # Test replacing the first element of a closed tuple + assert tuple_replace_at(tuple([integer(), atom()]), 0, boolean()) == + tuple([boolean(), atom()]) + + # Test replacing the last element of a closed tuple + assert tuple_replace_at(tuple([integer(), atom()]), 1, boolean()) == + tuple([integer(), boolean()]) + + # Test replacing in an open tuple + assert tuple_replace_at(open_tuple([integer(), atom(), boolean()]), 1, float()) == + open_tuple([integer(), float(), boolean()]) + + # Test replacing with a dynamic type + assert tuple_replace_at(tuple([integer(), atom()]), 1, dynamic()) == + dynamic(tuple([integer(), term()])) + + # Test replacing in a dynamic tuple + assert tuple_replace_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == + dynamic(tuple([integer(), boolean()])) + + # Test replacing in a union of tuples + assert tuple_replace_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == + tuple([boolean()]) + + # Test replacing in an intersection of tuples + assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) + |> tuple_replace_at(1, float()) == tuple([integer(), float()]) + + # Replacing in a difference where the negation actually constrains the + # positive (not just by arity). The replaced position drops its negative + # constraint, the other positions keep theirs. + assert difference(tuple([atom(), atom()]), tuple([atom([:a]), term()])) + |> tuple_replace_at(0, boolean()) + |> equal?(tuple([boolean(), atom()])) + + assert difference(tuple([atom(), atom()]), tuple([atom([:a]), term()])) + |> tuple_replace_at(1, boolean()) + |> equal?(difference(tuple([atom(), boolean()]), tuple([atom([:a]), boolean()]))) + + # Errors must propagate even when the replacement value is dynamic + assert tuple_replace_at(integer(), 0, dynamic()) == :badtuple + assert tuple_replace_at(term(), 0, dynamic()) == :badtuple + assert tuple_replace_at(tuple([atom([:ok])]), 1, dynamic()) == :badindex + assert tuple_replace_at(empty_tuple(), 0, dynamic()) == :badindex + + # Out-of-bounds writes to a dynamic fixed-size tuple must fail with :badindex + assert tuple_replace_at(dynamic(tuple([atom([:ok]), term()])), 2, binary()) == + :badindex + + assert tuple_replace_at(dynamic(tuple([atom([:ok])])), 1, binary()) == :badindex + + # Test replacing in a complex union involving dynamic + assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + |> tuple_replace_at(1, boolean()) + |> equal?( + union( + tuple([integer(), boolean()]), + dynamic(tuple([float(), boolean()])) + ) + ) + + # Successfully replacing at position `index` in a tuple means that the dynamic + # values that succeed are intersected with tuples of size at least `index + 1` + assert dynamic(tuple()) + |> tuple_replace_at(0, boolean()) + |> equal?(dynamic(open_tuple([boolean()]))) + + assert dynamic(term()) + |> tuple_replace_at(0, boolean()) + |> equal?(dynamic(open_tuple([boolean()]))) + + assert dynamic(union(tuple(), integer())) + |> tuple_replace_at(1, boolean()) + |> equal?(dynamic(open_tuple([term(), boolean()]))) + end + test "tuple_values" do assert tuple_values(term()) == :badtuple assert tuple_values(dynamic()) == dynamic() diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 5ace329d3a..2c9c50974b 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -838,6 +838,16 @@ defmodule Module.Types.ExprTest do assert typecheck!(Tuple.insert_at({:ok, 123}, 2, "foo")) == tuple([atom([:ok]), integer(), binary()]) + assert typeerror!( + [x], + ( + value = if :rand.uniform() > 0.5, do: :inserted, else: x + tuple = Tuple.insert_at({:ok}, 1, value) + Integer.to_string(elem(tuple, 1)) + ) + ) + |> strip_ansi() =~ "incompatible types given to Integer.to_string/1" + assert typeerror!([<>], Tuple.insert_at(x, 0, "foo")) |> strip_ansi() == ~l""" incompatible types given to Tuple.insert_at/3: @@ -932,6 +942,77 @@ defmodule Module.Types.ExprTest do """ end + test "put_elem/3" do + assert typecheck!(put_elem({:ok, 123}, 0, "foo")) == tuple([binary(), integer()]) + assert typecheck!(put_elem({:ok, 123}, 1, "foo")) == tuple([atom([:ok]), binary()]) + + assert typecheck!([x], put_elem({:ok, x}, 0, "foo")) == + dynamic(tuple([binary(), term()])) + + assert typecheck!([x], put_elem({:ok, x}, 1, "foo")) == + dynamic(tuple([atom([:ok]), binary()])) + + assert typeerror!([<>], put_elem(x, 0, "foo")) |> strip_ansi() == + ~l""" + incompatible types given to Kernel.put_elem/3: + + put_elem(x, 0, "foo") + + given types: + + float(), integer(), binary() + + but expected one of: + + {...}, integer(), term() + + where "x" was given the type: + + # type: float() + # from: types_test.ex:LINE-1 + <> + """ + + assert typeerror!(put_elem({:ok, 123}, 2, "foo")) == + ~l""" + expected a tuple with at least 3 elements in Kernel.put_elem/3: + + put_elem({:ok, 123}, 2, "foo") + + the given type does not have the given index: + + {:ok, integer()} + """ + + assert typeerror!([x], put_elem({:ok, x}, 2, "foo")) |> strip_ansi() == + ~l""" + expected a tuple with at least 3 elements in Kernel.put_elem/3: + + put_elem({:ok, x}, 2, "foo") + + the given type does not have the given index: + + dynamic({:ok, term()}) + + where "x" was given the type: + + # type: dynamic() + # from: types_test.ex:LINE-1 + x + """ + + assert typeerror!(put_elem({1, 2}, -1, :x)) == + ~l""" + expected a non-negative integer as index in Kernel.put_elem/3: + + put_elem({1, 2}, -1, :x) + + got the index: + + -1 + """ + end + test "Tuple.duplicate/2" do assert typecheck!(Tuple.duplicate(123, 0)) == tuple([]) assert typecheck!(Tuple.duplicate(123, 1)) == tuple([integer()])