Skip to content
Merged
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 @@ -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)))
Expand Down
32 changes: 29 additions & 3 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()]))
Expand Down Expand Up @@ -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()
Expand Down
81 changes: 81 additions & 0 deletions lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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!([<<x::float>>], Tuple.insert_at(x, 0, "foo")) |> strip_ansi() ==
~l"""
incompatible types given to Tuple.insert_at/3:
Expand Down Expand Up @@ -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!([<<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
Loading