Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tuple type operators for insertion and deletion #13912

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
148 changes: 144 additions & 4 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
[] -> []
Expand Down Expand Up @@ -1393,6 +1400,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)

Expand Down Expand Up @@ -1532,15 +1540,147 @@ 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)
{tag, elements, Enum.reject(negs, &tuple_empty_negation?(tag, n, &1))}
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? -> :badrange
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? ->
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)

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

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? -> :badrange
true -> :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? ->
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)

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

true ->
:badtuple
end
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} ->
{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()

# Remove useless negations, which denote tuples of incompatible sizes.
defp tuple_empty_negation?(tag, n, {neg_tag, neg_elements}) do
m = length(neg_elements)
Expand Down
115 changes: 114 additions & 1 deletion lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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()])))
Expand Down Expand Up @@ -474,6 +474,119 @@ defmodule Module.Types.DescrTest do
{true, union(atom(), dynamic())}
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()])

# 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 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()]))))

# 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
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()])

# 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 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()]))
)
)

# 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
assert map_fetch(term(), :a) == :badmap
assert map_fetch(union(open_map(), integer()), :a) == :badmap
Expand Down
Loading