Skip to content

Commit e303c78

Browse files
authored
Rework tuple projection algorithm (#15668)
1 parent 0f83205 commit e303c78

2 files changed

Lines changed: 138 additions & 92 deletions

File tree

lib/elixir/lib/module/types/descr.ex

Lines changed: 91 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5286,100 +5286,93 @@ defmodule Module.Types.Descr do
52865286
end
52875287

52885288
defp tuple_bdd_fetch_static(bdd, index) do
5289-
bdd
5290-
|> tuple_bdd_to_dnf_with_negations()
5291-
|> Enum.reduce({none(), false}, fn
5292-
# Optimization: if there are no negatives
5293-
{tag, elements, []}, {acc_descr, acc_optional?} ->
5294-
{descr, optional?} = tuple_fetch_element(elements, index, tag)
5295-
{opt_union(descr, acc_descr), optional? or acc_optional?}
5289+
{descr, optional?} =
5290+
bdd
5291+
|> tuple_bdd_to_dnf_with_negations()
5292+
|> Enum.reduce_while({none(), false}, fn
5293+
# Optimization: if there are no negatives
5294+
{tag, elements, []}, {acc_descr, acc_optional?} ->
5295+
{descr, optional?} = tuple_fetch_element(elements, index, tag)
5296+
5297+
{opt_union(descr, acc_descr), optional? or acc_optional?}
5298+
|> tuple_fetch_halt_if_saturated()
5299+
5300+
{tag, elements, negs}, {acc_descr, acc_optional?} = acc ->
5301+
result =
5302+
case tuple_take_element(elements, index, tag) do
5303+
:empty ->
5304+
acc
52965305

5297-
{tag, elements, negs}, {acc_descr, acc_optional?} ->
5298-
{_, value, bdd} = tuple_take_element(elements, index, tag)
5306+
{value, bdd} ->
5307+
negative = tuple_split_negative_pairs(negs, index)
52995308

5300-
case tuple_split_negative_pairs_index(negs, index) do
5301-
:empty ->
5302-
{acc_descr, acc_optional?}
5309+
value =
5310+
if tuple_pair_projection_keeps_full_fst?(negative, bdd) do
5311+
value
5312+
else
5313+
negative
5314+
|> tuple_project_negative_pairs(value, bdd)
5315+
|> Enum.reduce(none(), fn {field, _}, acc ->
5316+
opt_union(field, acc)
5317+
end)
5318+
end
53035319

5304-
negative ->
5305-
value =
5306-
if tuple_pair_projection_keeps_full_fst?(negative, bdd) do
5307-
value
5308-
else
5309-
negs
5310-
|> tuple_split_negative(index, value, bdd)
5311-
|> Enum.reduce({none(), false}, fn {field, _}, acc ->
5312-
field_opt_union(field, acc, %{})
5313-
end)
5314-
end
5320+
{opt_union(value, acc_descr), acc_optional?}
5321+
end
53155322

5316-
{descr, optional?} = value
5317-
{opt_union(descr, acc_descr), optional? or acc_optional?}
5318-
end
5319-
end)
5320-
catch
5321-
:open -> {term(), true}
5323+
tuple_fetch_halt_if_saturated(result)
5324+
end)
5325+
5326+
{descr, optional? or not tuple_of_size_at_least_static?(%{tuple: bdd}, index + 1)}
53225327
end
53235328

5324-
# Remove negatives:
5325-
# {t, s} \ {t₁, s₁} = {t \ t₁, s} ∪ {t ∩ t₁, s \ s₁}
5326-
defp tuple_split_negative(negs, index, value, bdd) do
5327-
Enum.reduce(negs, [{value, bdd}], fn
5328-
bdd_leaf(:open, []), _acc ->
5329-
throw(:empty)
5329+
defp tuple_fetch_halt_if_saturated({:term, true} = result), do: {:halt, result}
5330+
defp tuple_fetch_halt_if_saturated(result), do: {:cont, result}
53305331

5331-
bdd_leaf(neg_tag, neg_elements), acc ->
5332-
{found?, neg_value, neg_bdd} = tuple_take_element(neg_elements, index, neg_tag)
5332+
# Projects a difference of field/remainder products. The literals
5333+
# have been mapped through the inverse image of tuple insertion, so
5334+
# no optional-field cases remain.
5335+
# Removal formula is {t, s} \ {t₁, s₁} = {t \ t₁, s} ∪ {t ∩ t₁, s \ s₁}
5336+
defp tuple_project_negative_pairs(negative, value, bdd) do
5337+
Enum.reduce(negative, [{value, bdd}], fn {neg_value, neg_bdd}, acc ->
5338+
Enum.reduce(acc, [], fn {field, bdd}, acc ->
5339+
# If the negative tag is closed, then they are likely disjoint,
5340+
# so we can drastically cut down the amount of operations.
5341+
if match?(bdd_leaf(:closed, _), neg_bdd) and
5342+
tuple_empty?(tuple_intersection(bdd, neg_bdd), %{}) do
5343+
[{field, bdd} | acc]
5344+
else
5345+
intersection_field = opt_intersection(field, neg_value)
53335346

5334-
if not found? and neg_tag == :open do
5335-
# In case the tuple is open, t \ t₁ is always empty,
5336-
# t ∩ t₁ is always t, so we just need to deal with the bdd.
5337-
Enum.reduce(acc, [], fn {field, bdd}, acc ->
5347+
if empty?(intersection_field) do
5348+
[{field, bdd} | acc]
5349+
else
53385350
diff_bdd = tuple_difference(bdd, neg_bdd)
53395351

53405352
if tuple_empty?(diff_bdd, %{}) do
5341-
acc
5353+
prepend_tuple_pair_unless_empty_diff(field, neg_value, bdd, acc)
53425354
else
5343-
[{field, diff_bdd} | acc]
5355+
acc = [{intersection_field, diff_bdd} | acc]
5356+
prepend_tuple_pair_unless_empty_diff(field, neg_value, bdd, acc)
53445357
end
5345-
end)
5346-
else
5347-
Enum.reduce(acc, [], fn {field, bdd}, acc ->
5348-
# If the negative tag is closed, then they are likely disjoint,
5349-
# so we can drastically cut down the amount of operations.
5350-
if neg_tag == :closed and tuple_empty?(tuple_intersection(bdd, neg_bdd), %{}) do
5351-
[{field, bdd} | acc]
5352-
else
5353-
intersection_field = field_intersection(field, neg_value)
5354-
5355-
if field_empty?(intersection_field) do
5356-
[{field, bdd} | acc]
5357-
else
5358-
diff_bdd = tuple_difference(bdd, neg_bdd)
5359-
5360-
if tuple_empty?(diff_bdd, %{}) do
5361-
prepend_map_pair_unless_empty_diff(field, neg_value, bdd, acc)
5362-
else
5363-
acc = [{intersection_field, diff_bdd} | acc]
5364-
prepend_map_pair_unless_empty_diff(field, neg_value, bdd, acc)
5365-
end
5366-
end
5367-
end
5368-
end)
5358+
end
53695359
end
5360+
end)
53705361
end)
5371-
catch
5372-
:empty -> []
53735362
end
53745363

5375-
defp tuple_split_negative_pairs_index(negs, index) do
5376-
Enum.reduce_while(negs, [], fn
5377-
bdd_leaf(:open, []), _acc ->
5378-
{:halt, :empty}
5364+
defp prepend_tuple_pair_unless_empty_diff(value, neg_value, bdd, acc) do
5365+
diff = opt_difference(value, neg_value)
5366+
if empty?(diff), do: acc, else: [{diff, bdd} | acc]
5367+
end
53795368

5380-
bdd_leaf(tag, elements), neg_acc ->
5381-
{_found?, neg_value, neg_bdd} = tuple_take_element(elements, index, tag)
5382-
{:cont, [{neg_value, neg_bdd} | neg_acc]}
5369+
# Splits every negative tuple literal into its value/remainder pair at `index`.
5370+
defp tuple_split_negative_pairs(negs, index) do
5371+
Enum.reduce(negs, [], fn bdd_leaf(tag, elements), acc ->
5372+
case tuple_take_element(elements, index, tag) do
5373+
:empty -> acc
5374+
pair -> [pair | acc]
5375+
end
53835376
end)
53845377
end
53855378

@@ -5399,32 +5392,33 @@ defmodule Module.Types.Descr do
53995392

54005393
defp tuple_pair_projection_keeps_full_snd?(negative, value) do
54015394
neg_values =
5402-
Enum.reduce(negative, {none(), false}, fn {neg_value, _neg_bdd}, acc ->
5403-
field_union(neg_value, acc)
5395+
Enum.reduce(negative, none(), fn {neg_value, _neg_bdd}, acc ->
5396+
opt_union(neg_value, acc)
54045397
end)
54055398

5406-
not field_empty?(field_difference(value, neg_values))
5399+
not empty?(opt_difference(value, neg_values))
54075400
end
54085401

54095402
defp tuple_fetch_element([], _, :open), do: {term(), true}
54105403
defp tuple_fetch_element([], _, :closed), do: {none(), true}
54115404
defp tuple_fetch_element([h | _], 0, _tag), do: {h, false}
54125405
defp tuple_fetch_element([_ | t], i, tag), do: tuple_fetch_element(t, i - 1, tag)
54135406

5407+
# Computes the inverse image of a tuple literal under insertion at `index`:
5408+
# {value, remainder} represents all pairs whose insertion belongs to the
5409+
# literal.
54145410
defp tuple_take_element(elements, index, tag) do
54155411
case do_tuple_take_element(elements, index, []) do
5416-
:error -> {false, tuple_tag_to_field(tag), tuple_new(tag, elements)}
5417-
{value, elements} -> {true, {value, false}, tuple_new(tag, elements)}
5412+
:error when tag == :closed -> :empty
5413+
:error -> {term(), tuple_new(:open, tuple_fill(elements, index))}
5414+
{value, elements} -> {value, tuple_new(tag, elements)}
54185415
end
54195416
end
54205417

54215418
defp do_tuple_take_element([], _, _), do: :error
54225419
defp do_tuple_take_element([h | t], 0, acc), do: {h, Enum.reverse(acc, t)}
54235420
defp do_tuple_take_element([h | t], i, acc), do: do_tuple_take_element(t, i - 1, [h | acc])
54245421

5425-
defp tuple_tag_to_field(:open), do: {term(), true}
5426-
defp tuple_tag_to_field(:closed), do: {none(), false}
5427-
54285422
@doc """
54295423
Returns all of the values that are part of a tuple.
54305424
"""
@@ -5526,22 +5520,27 @@ defmodule Module.Types.Descr do
55265520
|> tuple_bdd_to_dnf_with_negations()
55275521
|> Enum.reduce(:bdd_bot, fn
55285522
{tag, elements, []}, acc ->
5529-
{_, _, bdd} = tuple_take_element(elements, index, tag)
5530-
opt_tuple_union(bdd, acc)
5523+
case tuple_take_element(elements, index, tag) do
5524+
:empty ->
5525+
acc
55315526

5532-
{tag, elements, negs}, acc ->
5533-
{_, value, bdd} = tuple_take_element(elements, index, tag)
5527+
{_value, bdd} ->
5528+
opt_tuple_union(bdd, acc)
5529+
end
55345530

5535-
case tuple_split_negative_pairs_index(negs, index) do
5531+
{tag, elements, negs}, acc ->
5532+
case tuple_take_element(elements, index, tag) do
55365533
:empty ->
55375534
acc
55385535

5539-
negative ->
5536+
{value, bdd} ->
5537+
negative = tuple_split_negative_pairs(negs, index)
5538+
55405539
if tuple_pair_projection_keeps_full_snd?(negative, value) do
55415540
opt_tuple_union(bdd, acc)
55425541
else
5543-
negs
5544-
|> tuple_split_negative(index, value, bdd)
5542+
negative
5543+
|> tuple_project_negative_pairs(value, bdd)
55455544
|> Enum.reduce(acc, fn {_, bdd}, acc -> opt_tuple_union(bdd, acc) end)
55465545
end
55475546
end

lib/elixir/test/elixir/module/types/descr_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,6 +1976,24 @@ defmodule Module.Types.DescrTest do
19761976
assert tuple_fetch(tuple(), 0) == :badindex
19771977

19781978
assert tuple_fetch(projected_negative_tuple(200), 1) == {false, term()}
1979+
1980+
for minimum_size <- 1..4 do
1981+
by_negation =
1982+
Enum.reduce(0..(minimum_size - 1), open_tuple([]), fn arity, acc ->
1983+
opt_difference(acc, tuple(List.duplicate(term(), arity)))
1984+
end)
1985+
1986+
for index <- 0..(minimum_size - 1) do
1987+
assert tuple_fetch(by_negation, index) == {false, term()}
1988+
end
1989+
end
1990+
1991+
# Optionality is not representation-dependent.
1992+
with_optional = opt_union(tuple([list(term()), empty_map()]), tuple([term()]))
1993+
rewritten = opt_difference(term(), opt_difference(term(), with_optional))
1994+
assert equal?(with_optional, rewritten)
1995+
assert tuple_fetch(with_optional, 1) == :badindex
1996+
assert tuple_fetch(rewritten, 1) == :badindex
19791997
end
19801998

19811999
test "tuple_fetch with dynamic" do
@@ -2051,6 +2069,35 @@ defmodule Module.Types.DescrTest do
20512069
assert dynamic(opt_union(tuple(), integer()))
20522070
|> tuple_delete_at(1)
20532071
|> equal?(dynamic(tuple_of_size_at_least(1)))
2072+
2073+
# Deletion must be independent of representation.
2074+
# Size lower bounds carried only by closed negations must project exactly,
2075+
# at every valid index and independently of their BDD representation.
2076+
for minimum_size <- 1..6 do
2077+
canonical = open_tuple(List.duplicate(term(), minimum_size))
2078+
2079+
by_negation =
2080+
Enum.reduce(0..(minimum_size - 1), open_tuple([]), fn arity, acc ->
2081+
opt_difference(acc, tuple(List.duplicate(term(), arity)))
2082+
end)
2083+
2084+
assert equal?(canonical, by_negation)
2085+
2086+
expected = open_tuple(List.duplicate(term(), minimum_size - 1))
2087+
2088+
for index <- 0..(minimum_size - 1) do
2089+
assert by_negation
2090+
|> tuple_delete_at(index)
2091+
|> equal?(expected)
2092+
end
2093+
end
2094+
2095+
# An open negative whose explicit prefix stops before the deleted index
2096+
# must still have an exact insertion inverse image.
2097+
assert open_tuple([term(), term()])
2098+
|> opt_difference(open_tuple([atom()]))
2099+
|> tuple_delete_at(1)
2100+
|> equal?(opt_difference(open_tuple([term()]), open_tuple([atom()])))
20542101
end
20552102

20562103
test "tuple_insert_at" do

0 commit comments

Comments
 (0)