Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions lib/elixir/lib/module/types/apply.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
89 changes: 89 additions & 0 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 92 additions & 0 deletions lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
71 changes: 71 additions & 0 deletions lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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!([<<x::float>>], 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
<<x::float>>
"""

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()])
Expand Down