diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 8f941e4124..2a6a928f02 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -492,6 +492,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) and index < 1 do {_, context} = of_fun.(tuple, open_tuple([]), expr, stack, context) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index bbef71f22a..2cc9b6380f 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5919,6 +5919,95 @@ 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(:term, _key, _type), do: :badtuple + + def tuple_replace_at(descr, index, type) when is_integer(index) and index >= 0 do + case :maps.take(:dynamic, unfold(type)) do + :error -> + tuple_replace_at_checked(descr, index, type) + + {dynamic_type, _static} -> + case tuple_replace_at_checked(descr, index, dynamic_type) do + atom when atom in [:badtuple, :badindex] -> atom + result -> dynamic(result) + end + end + end + + def tuple_replace_at(_, _, _), do: :badindex + + defp tuple_replace_at_checked(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 non_empty_tuple_only?(descr) + is_proper_size? = tuple_of_size_at_least_static?(descr, index + 1) + + cond do + is_proper_tuple? and is_proper_size? -> tuple_replace_static(descr, index, type) + is_proper_tuple? -> :badindex + true -> :badtuple + end + + {dynamic, static} -> + is_proper_tuple? = descr_key?(dynamic, :tuple) and tuple_only?(static) + is_proper_size? = tuple_of_size_at_least_static?(static, index + 1) + + cond do + is_proper_tuple? and is_proper_size? -> + static_result = tuple_replace_static(static, index, type) + + # Prune for dynamic values that make the operation succeed. + dynamic_input = intersection(dynamic, tuple_of_size_at_least(index + 1)) + + if empty?(dynamic_input) and empty?(static) do + :badindex + else + dynamic_result = tuple_replace_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? -> + :badindex + + true -> + :badtuple + end + end + end + + defp tuple_replace_static(descr, _, _) when descr == @none, do: none() + + # Unlike insert/delete, replace is not injective at the replaced index, so + # transformed negative leaves would discard valid outputs. We eliminate + # negations first by converting to a positive-only DNF and replacing in + # each disjunct. + defp tuple_replace_static(descr, index, type) do + Map.update!(descr, :tuple, fn bdd -> + tuple_bdd_to_dnf_no_negations(bdd) + |> Enum.reduce(:bdd_bot, fn {tag, elements}, acc -> + # If the tuple is open, then we want List.replace_at to update the correct + # index, which requires filling the tuple with `term()` values first. + # Closed tuples of an incorrect size will have been cancelled by the + # earlier intersection with `tuple_of_size_at_least`. + elements = + if tag == :open and length(elements) < index + 1 do + tuple_fill(elements, index + 1) + else + elements + end + + bdd_union(acc, tuple_new(tag, List.replace_at(elements, index, type))) + end) + end) + end + 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 53c49536b7..1ee1486686 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1883,6 +1883,98 @@ defmodule Module.Types.DescrTest do |> equal?(dynamic(open_tuple([term(), boolean()]))) 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..9fa57f16c2 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -932,6 +932,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()])