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
160 changes: 134 additions & 26 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5584,15 +5584,26 @@ defmodule Module.Types.Descr do
{optional?, descr} = tuple_fetch_element(elements, index, tag)
{optional? or acc_optional?, union(descr, acc_descr)}

{tag, elements, negs}, acc ->
{tag, elements, negs}, {acc_optional?, acc_descr} ->
{_, value, bdd} = tuple_take_element(elements, index, tag)

negs
|> tuple_split_negative(index, value, bdd)
|> Enum.reduce(acc, fn {value, _}, {acc_optional?, acc_descr} ->
{optional?, descr} = pop_optional_static(value)
{optional? or acc_optional?, union(descr, acc_descr)}
end)
case tuple_split_negative_pairs_index(negs, index) do
:empty ->
{acc_optional?, acc_descr}

negative ->
value =
if tuple_pair_projection_keeps_full_fst?(negative, bdd) do
value
else
negs
|> tuple_split_negative(index, value, bdd)
|> Enum.reduce(none(), fn {value, _}, acc -> union(value, acc) end)
end

{optional?, descr} = pop_optional_static(value)
{optional? or acc_optional?, union(descr, acc_descr)}
end
end)
catch
:open -> {true, term()}
Expand Down Expand Up @@ -5649,6 +5660,40 @@ defmodule Module.Types.Descr do
:empty -> []
end

defp tuple_split_negative_pairs_index(negs, index) do
Enum.reduce_while(negs, [], fn
bdd_leaf(:open, []), _acc ->
{:halt, :empty}

bdd_leaf(tag, elements), neg_acc ->
{_found?, neg_value, neg_bdd} = tuple_take_element(elements, index, tag)
{:cont, [{neg_value, neg_bdd} | neg_acc]}
end)
end

# Projection shortcuts for the pair-shaped tuple split below. These are
# existential checks: if at least one remaining-tuple sample avoids all
# negative remaining tuples, the full value side survives; dually, if at least
# one value sample avoids all negative values, the full remaining-tuple side
# survives. If neither shortcut applies, we fall back to the regular split.
defp tuple_pair_projection_keeps_full_fst?(negative, bdd) do
neg_bdd =
Enum.reduce(negative, :bdd_bot, fn {_neg_value, neg_bdd}, acc ->
tuple_union(neg_bdd, acc)
end)

not tuple_empty?(tuple_difference(bdd, neg_bdd))
end

defp tuple_pair_projection_keeps_full_snd?(negative, value) do
neg_values =
Enum.reduce(negative, none(), fn {neg_value, _neg_bdd}, acc ->
union(neg_value, acc)
end)

not empty?(difference(value, neg_values))
end

defp tuple_fetch_element([], _, :open), do: {true, term()}
defp tuple_fetch_element([], _, :closed), do: {true, none()}
defp tuple_fetch_element([h | _], 0, _tag), do: {false, h}
Expand Down Expand Up @@ -5740,12 +5785,15 @@ defmodule Module.Types.Descr 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)
# Prune for dynamic values that make the operation succeed.
dynamic_input = intersection(dynamic, tuple_of_size_at_least(index + 1))

union(dynamic(dynamic_result), static_result)
if empty?(dynamic_input) and empty?(static) do
:badindex
else
dynamic_result = tuple_delete_static(dynamic_input, index)
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? ->
Expand All @@ -5759,17 +5807,38 @@ defmodule Module.Types.Descr do

def tuple_delete_at(_, _), do: :badindex

# Takes a static map type and removes an index from it.
# Takes a static tuple type and removes an index from it.
defp tuple_delete_static(%{tuple: bdd}, index) do
%{
tuple:
bdd_map(bdd, fn bdd_leaf(tag, elements) ->
bdd_leaf_new(tag, List.delete_at(elements, index))
end)
}
bdd =
bdd
|> tuple_bdd_to_dnf_with_negations()
|> Enum.reduce(:bdd_bot, fn
{tag, elements, []}, acc ->
{_, _, bdd} = tuple_take_element(elements, index, tag)
tuple_union(bdd, acc)

{tag, elements, negs}, acc ->
{_, value, bdd} = tuple_take_element(elements, index, tag)

case tuple_split_negative_pairs_index(negs, index) do
:empty ->
acc

negative ->
if tuple_pair_projection_keeps_full_snd?(negative, value) do
tuple_union(bdd, acc)
else
negs
|> tuple_split_negative(index, value, bdd)
|> Enum.reduce(acc, fn {_, bdd}, acc -> tuple_union(bdd, acc) end)
end
end
end)

%{tuple: bdd}
end

# If there is no map part to this static type, there is nothing to delete.
# If there is no tuple part to this static type, there is nothing to delete.
defp tuple_delete_static(_type, _key), do: none()

@doc """
Expand Down Expand Up @@ -5810,11 +5879,14 @@ defmodule Module.Types.Descr do
static_result = tuple_insert_static(static, index, type)

# Prune for dynamic values that make the intersection succeed
dynamic_result =
intersection(dynamic, tuple_of_size_at_least(index))
|> tuple_insert_static(index, type)
dynamic_input = intersection(dynamic, tuple_of_size_at_least(index))

union(dynamic(dynamic_result), static_result)
if empty?(dynamic_input) and empty?(static) do
:badindex
else
dynamic_result = tuple_insert_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? ->
Expand Down Expand Up @@ -5854,14 +5926,50 @@ defmodule Module.Types.Descr do
defp tuple_of_size_at_least_static?(descr, index) do
case descr do
%{tuple: bdd} ->
tuple_bdd_to_dnf_no_negations(bdd)
|> Enum.all?(fn {_, elements} -> length(elements) >= index end)
tuple_bdd_positive_size_at_least?(bdd, index) or
tuple_empty?(tuple_difference(bdd, tuple_new(:open, List.duplicate(term(), index))))

%{} ->
true
end
end

# This is a cheap proof that a tuple type has a size lower bound.
#
# In an intersection of tuples, the size is lower-bounded by each tuple literal
# we encounter. So we can find a lower bound on the size by making sure that all
# paths in the bdd have at least one tuple literal with a size >= to index.
#
# We don't need to check that the elements are not equivalent to none(), since in
# that case the intersection will be equivalent to none() and will disappear.
#
# This misses the case where the lower bound is proven by negations. For instance,
# the type "{...} and not {}" has a size of at least 1.
defp tuple_bdd_positive_size_at_least?(_bdd, 0), do: true

defp tuple_bdd_positive_size_at_least?(bdd, index),
do: tuple_bdd_positive_size_at_least?(bdd, index, false)

defp tuple_bdd_positive_size_at_least?(_bdd, _index, true), do: true
defp tuple_bdd_positive_size_at_least?(:bdd_bot, _index, _guaranteed?), do: true
defp tuple_bdd_positive_size_at_least?(:bdd_top, _index, guaranteed?), do: guaranteed?

defp tuple_bdd_positive_size_at_least?(bdd_leaf(_tag, elements), index, _guaranteed?) do
length(elements) >= index
end

defp tuple_bdd_positive_size_at_least?(
{_, bdd_leaf(_tag, elements), c, u, d},
index,
guaranteed?
) do
literal_guaranteed? = length(elements) >= index

tuple_bdd_positive_size_at_least?(u, index, guaranteed?) and
tuple_bdd_positive_size_at_least?(c, index, literal_guaranteed?) and
tuple_bdd_positive_size_at_least?(d, index, guaranteed?)
end

## BDD helpers

# Some of our operations rely on eliminating closed tuples/maps,
Expand Down
32 changes: 32 additions & 0 deletions lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ defmodule Module.Types.DescrTest do
end)
end

defp projected_negative_tuple(size) do
Enum.reduce(1..size, open_tuple([open_tuple([]), term()]), fn index, acc ->
difference(
acc,
open_tuple([
open_tuple([atom([:"value#{index}"])]),
integer()
])
)
end)
end

describe "union" do
test "bitmap" do
assert union(integer(), float()) == union(float(), integer())
Expand Down Expand Up @@ -1719,6 +1731,8 @@ defmodule Module.Types.DescrTest do
|> tuple_fetch(2) == {false, integer()}

assert tuple_fetch(tuple(), 0) == :badindex

assert tuple_fetch(projected_negative_tuple(200), 1) == {false, term()}
end

test "tuple_fetch with dynamic" do
Expand Down Expand Up @@ -1759,6 +1773,8 @@ defmodule Module.Types.DescrTest do
assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 1) ==
dynamic(tuple([integer()]))

assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 2) == :badindex

# Test deleting from a union of tuples
assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1)
|> equal?(tuple([union(integer(), float())]))
Expand All @@ -1772,6 +1788,13 @@ defmodule Module.Types.DescrTest do
|> tuple_delete_at(1)
|> equal?(tuple([integer(), boolean()]))

assert difference(
open_tuple([open_tuple([]), term()]),
open_tuple([open_tuple([atom([:value])]), integer()])
)
|> tuple_delete_at(1)
|> equal?(open_tuple([open_tuple([])]))

# Test deleting from a complex union involving dynamic
assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()])))
|> tuple_delete_at(1)
Expand Down Expand Up @@ -1825,10 +1848,19 @@ defmodule Module.Types.DescrTest do
assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) ==
dynamic(tuple([integer(), boolean(), atom()]))

assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 3, boolean()) == :badindex

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

assert difference(tuple(), empty_tuple())
|> tuple_insert_at(1, boolean())
|> equal?(open_tuple([term(), boolean()]))

inserted = tuple_insert_at(projected_negative_tuple(200), 1, atom([:inserted]))
assert tuple_fetch(inserted, 1) == {false, atom([:inserted])}

# Test inserting into a difference of tuples
assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()]))
|> tuple_insert_at(1, float())
Expand Down
Loading