Skip to content

Commit 472a49d

Browse files
authored
Preserve static parts in dynamic container descriptors (#15363)
1 parent a631da4 commit 472a49d

4 files changed

Lines changed: 155 additions & 90 deletions

File tree

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

Lines changed: 95 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,9 +2143,27 @@ defmodule Module.Types.Descr do
21432143
#
21442144
# none() types can be given and, while stored, it means the list type is empty.
21452145
defp list_descr(list_type, last_type, empty?) do
2146-
{list_dynamic?, list_type} = list_pop_dynamic(list_type)
2147-
{last_dynamic?, last_type} = list_pop_dynamic(last_type)
2146+
dynamic? = gradual?(list_type) or gradual?(last_type)
2147+
{dynamic_list_type, static_list_type} = pop_dynamic(list_type)
2148+
{dynamic_last_type, static_last_type} = pop_dynamic(last_type)
2149+
dynamic_descr = list_descr_static(dynamic_list_type, dynamic_last_type, empty?)
2150+
# Just a syntactic check, to avoid a recursive empty? call
2151+
static_empty? = static_list_type == @none or static_last_type == @none
21482152

2153+
cond do
2154+
not dynamic? ->
2155+
dynamic_descr
2156+
2157+
static_empty? ->
2158+
%{dynamic: dynamic_descr}
2159+
2160+
true ->
2161+
list_descr_static(static_list_type, static_last_type, empty?)
2162+
|> Map.put(:dynamic, dynamic_descr)
2163+
end
2164+
end
2165+
2166+
defp list_descr_static(list_type, last_type, empty?) do
21492167
list_part =
21502168
if last_type == :term do
21512169
list_new(:term, :term)
@@ -2176,13 +2194,7 @@ defmodule Module.Types.Descr do
21762194
end
21772195
end
21782196

2179-
list_descr =
2180-
if empty?, do: %{list: list_part, bitmap: @bit_empty_list}, else: %{list: list_part}
2181-
2182-
case list_dynamic? or last_dynamic? do
2183-
true -> %{dynamic: list_descr}
2184-
false -> list_descr
2185-
end
2197+
if empty?, do: %{list: list_part, bitmap: @bit_empty_list}, else: %{list: list_part}
21862198
end
21872199

21882200
defp list_new(list_type, last_type), do: bdd_leaf_new(list_type, last_type)
@@ -2231,15 +2243,6 @@ defmodule Module.Types.Descr do
22312243
end)
22322244
end
22332245

2234-
defp list_pop_dynamic(:term), do: {false, :term}
2235-
2236-
defp list_pop_dynamic(descr) do
2237-
case :maps.take(:dynamic, descr) do
2238-
:error -> {false, descr}
2239-
{dynamic, _} -> {true, dynamic}
2240-
end
2241-
end
2242-
22432246
defp list_tail_unfold(:term), do: @not_non_empty_list
22442247
defp list_tail_unfold(other), do: Map.delete(other, :list)
22452248

@@ -2784,8 +2787,19 @@ defmodule Module.Types.Descr do
27842787
defp domain_key_to_descr(:list), do: @list_top
27852788

27862789
defp map_descr(tag, pairs) do
2787-
{fields, domains, dynamic?} = map_descr_pairs(pairs, [], @fields_new, false)
2790+
{fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_empty?} =
2791+
map_descr_pairs(pairs)
2792+
2793+
dynamic_descr = map_descr_static(tag, dynamic_fields, dynamic_domains)
2794+
2795+
cond do
2796+
not dynamic? -> dynamic_descr
2797+
static_empty? -> %{dynamic: dynamic_descr}
2798+
true -> Map.put(map_descr_static(tag, fields, domains), :dynamic, dynamic_descr)
2799+
end
2800+
end
27882801

2802+
defp map_descr_static(tag, fields, domains) do
27892803
map_new =
27902804
if not is_fields_empty(domains) do
27912805
domains =
@@ -2801,10 +2815,7 @@ defmodule Module.Types.Descr do
28012815
map_new(tag, fields)
28022816
end
28032817

2804-
case dynamic? do
2805-
true -> %{dynamic: %{map: map_new}}
2806-
false -> %{map: map_new}
2807-
end
2818+
%{map: map_new}
28082819
end
28092820

28102821
defp map_put_domain(domain, domain_keys, value) when is_list(domain_keys) do
@@ -2825,30 +2836,34 @@ defmodule Module.Types.Descr do
28252836

28262837
defp map_put_domain(domain, [], _initial, _value), do: domain
28272838

2828-
defp map_descr_pairs([{key, :term} | rest], fields, domain, dynamic?) do
2829-
case is_atom(key) do
2830-
true -> map_descr_pairs(rest, [{key, :term} | fields], domain, dynamic?)
2831-
false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, :term), dynamic?)
2832-
end
2839+
defp map_descr_pairs(pairs) do
2840+
{fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_possible?} =
2841+
Enum.reduce(pairs, {[], @fields_new, [], @fields_new, false, false}, fn {key, value}, acc ->
2842+
map_descr_pair(key, value, acc)
2843+
end)
2844+
2845+
{fields_from_reverse_list(fields), domains, fields_from_reverse_list(dynamic_fields),
2846+
dynamic_domains, dynamic?, static_possible?}
28332847
end
28342848

2835-
defp map_descr_pairs([{key, value} | rest], fields, domain, dynamic?) do
2836-
{value, dynamic?} =
2837-
case :maps.take(:dynamic, value) do
2838-
:error -> {value, dynamic?}
2839-
{dynamic, _static} -> {dynamic, true}
2840-
end
2849+
defp map_descr_pair(
2850+
key,
2851+
value,
2852+
{fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_empty?}
2853+
) do
2854+
{dynamic_value, static_value} = pop_dynamic(value)
2855+
dynamic? = dynamic? or gradual?(value)
2856+
static_empty? = static_empty? or static_value == @none
28412857

2842-
case is_atom(key) do
2843-
true -> map_descr_pairs(rest, [{key, value} | fields], domain, dynamic?)
2844-
false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, value), dynamic?)
2858+
if is_atom(key) do
2859+
{[{key, static_value} | fields], domains, [{key, dynamic_value} | dynamic_fields],
2860+
dynamic_domains, dynamic?, static_empty?}
2861+
else
2862+
{fields, map_put_domain(domains, key, static_value), dynamic_fields,
2863+
map_put_domain(dynamic_domains, key, dynamic_value), dynamic?, static_empty?}
28452864
end
28462865
end
28472866

2848-
defp map_descr_pairs([], fields, domain, dynamic?) do
2849-
{fields_from_reverse_list(fields), domain, dynamic?}
2850-
end
2851-
28522867
# Gets the default type associated to atom keys in a map.
28532868
defp map_key_tag_to_type(:open), do: term_or_optional()
28542869
defp map_key_tag_to_type(:closed), do: not_set()
@@ -4965,46 +4980,34 @@ defmodule Module.Types.Descr do
49654980
# - {atom(), boolean(), ...} is encoded as {:open, [atom(), boolean()]}
49664981

49674982
defp tuple_descr(tag, fields) do
4968-
case tuple_descr(fields, [], false) do
4969-
:empty -> %{}
4970-
{fields, true} -> %{dynamic: %{tuple: tuple_new(tag, Enum.reverse(fields))}}
4971-
{_, false} -> %{tuple: tuple_new(tag, fields)}
4972-
end
4973-
end
4983+
{static_fields, dynamic_fields, dynamic?, static_empty?} =
4984+
tuple_descr_fields(fields, [], [], false, false)
4985+
4986+
dynamic_descr = tuple_descr_static(tag, dynamic_fields)
49744987

4975-
defp tuple_descr([:term | rest], acc, dynamic?) do
4976-
tuple_descr(rest, [:term | acc], dynamic?)
4988+
cond do
4989+
not dynamic? -> dynamic_descr
4990+
static_empty? -> %{dynamic: dynamic_descr}
4991+
true -> Map.put(tuple_descr_static(tag, static_fields), :dynamic, dynamic_descr)
4992+
end
49774993
end
49784994

4979-
defp tuple_descr([value | rest], acc, dynamic?) do
4980-
# Check if the static part is empty
4981-
static_empty? =
4982-
case value do
4983-
# Has dynamic component, check static separately
4984-
%{dynamic: _} -> false
4985-
_ -> empty?(value)
4986-
end
4995+
defp tuple_descr_static(tag, fields), do: %{tuple: tuple_new(tag, :lists.reverse(fields))}
49874996

4988-
if static_empty? do
4989-
:empty
4990-
else
4991-
case :maps.take(:dynamic, value) do
4992-
:error ->
4993-
tuple_descr(rest, [value | acc], dynamic?)
4997+
defp tuple_descr_fields([value | rest], acc, dynamic_acc, dynamic?, static_empty?) do
4998+
{dynamic_value, static_value} = pop_dynamic(value)
49944999

4995-
{dynamic, _static} ->
4996-
# Check if dynamic component is empty
4997-
if empty?(dynamic) do
4998-
:empty
4999-
else
5000-
tuple_descr(rest, [dynamic | acc], true)
5001-
end
5002-
end
5003-
end
5000+
tuple_descr_fields(
5001+
rest,
5002+
[static_value | acc],
5003+
[dynamic_value | dynamic_acc],
5004+
dynamic? or gradual?(value),
5005+
static_empty? or static_value == @none
5006+
)
50045007
end
50055008

5006-
defp tuple_descr([], acc, dynamic?) do
5007-
{acc, dynamic?}
5009+
defp tuple_descr_fields([], acc, dynamic_acc, dynamic?, static_empty?) do
5010+
{acc, dynamic_acc, dynamic?, static_empty?}
50085011
end
50095012

50105013
defp tuple_new(tag, elements), do: bdd_leaf_new(tag, elements)
@@ -5023,7 +5026,14 @@ defmodule Module.Types.Descr do
50235026
end
50245027
end
50255028

5026-
defp tuple_literal_intersection(:open, [], tag, elements), do: {tag, elements}
5029+
# Detecting tuples built with none() fields
5030+
defp tuple_literal_intersection(:open, [], tag, elements) do
5031+
if Enum.any?(elements, &empty?/1) do
5032+
:empty
5033+
else
5034+
{tag, elements}
5035+
end
5036+
end
50275037

50285038
defp tuple_literal_intersection(tag1, elements1, tag2, elements2) do
50295039
case tuple_sizes_strategy(tag1, length(elements1), tag2, length(elements2)) do
@@ -5050,8 +5060,13 @@ defmodule Module.Types.Descr do
50505060
defp tuple_sizes_strategy(_, _, _, _), do: :none
50515061

50525062
# Intersects two lists of types, and _appends_ the extra elements to the result.
5053-
defp zip_non_empty_intersection!([], types2, acc), do: Enum.reverse(acc, types2)
5054-
defp zip_non_empty_intersection!(types1, [], acc), do: Enum.reverse(acc, types1)
5063+
defp zip_non_empty_intersection!([], types2, acc) do
5064+
if Enum.any?(types2, &empty?/1), do: throw(:empty), else: Enum.reverse(acc, types2)
5065+
end
5066+
5067+
defp zip_non_empty_intersection!(types1, [], acc) do
5068+
if Enum.any?(types1, &empty?/1), do: throw(:empty), else: Enum.reverse(acc, types1)
5069+
end
50555070

50565071
defp zip_non_empty_intersection!([type1 | rest1], [type2 | rest2], acc) do
50575072
zip_non_empty_intersection!(rest1, rest2, [non_empty_intersection!(type1, type2) | acc])
@@ -5662,7 +5677,7 @@ defmodule Module.Types.Descr do
56625677
def tuple_values(descr) do
56635678
case :maps.take(:dynamic, descr) do
56645679
:error ->
5665-
if tuple_only?(descr) do
5680+
if non_empty_tuple_only?(descr) do
56665681
process_tuples_values(Map.get(descr, :tuple, :bdd_bot))
56675682
else
56685683
:badtuple
@@ -5708,7 +5723,7 @@ defmodule Module.Types.Descr do
57085723
case :maps.take(:dynamic, descr) do
57095724
:error ->
57105725
# Note: the empty type is not a valid input
5711-
is_proper_tuple? = descr_key?(descr, :tuple) and tuple_only?(descr)
5726+
is_proper_tuple? = descr_key?(descr, :tuple) and non_empty_tuple_only?(descr)
57125727
is_proper_size? = tuple_of_size_at_least_static?(descr, index + 1)
57135728

57145729
cond do
@@ -5777,7 +5792,7 @@ defmodule Module.Types.Descr do
57775792
case :maps.take(:dynamic, descr) do
57785793
:error ->
57795794
# Note: the empty type is not a valid input
5780-
is_proper_tuple? = descr_key?(descr, :tuple) and tuple_only?(descr)
5795+
is_proper_tuple? = descr_key?(descr, :tuple) and non_empty_tuple_only?(descr)
57815796
is_proper_size? = index == 0 or tuple_of_size_at_least_static?(descr, index)
57825797

57835798
cond do

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,21 @@ defmodule Module.Types.DescrTest do
304304

305305
assert intersection(tuple([term(), integer()]), tuple([atom(), term()]))
306306
|> equal?(tuple([atom(), integer()]))
307+
308+
empty_field =
309+
closed_map(key: atom([:value]))
310+
|> difference(open_map(key: atom(), optional: if_set(atom())))
311+
312+
assert empty?(empty_field)
313+
refute empty_field == none()
314+
315+
assert intersection(open_tuple([integer()]), tuple([integer(), empty_field]))
316+
|> equal?(none())
317+
318+
assert intersection(tuple([integer(), empty_field]), open_tuple([integer()]))
319+
|> equal?(none())
320+
321+
assert intersection(tuple(), tuple([integer(), empty_field])) |> equal?(none())
307322
end
308323

309324
test "map" do
@@ -822,7 +837,7 @@ defmodule Module.Types.DescrTest do
822837
test "map hoists dynamic" do
823838
assert dynamic(open_map(a: integer())) == open_map(a: dynamic(integer()))
824839

825-
assert dynamic(open_map(a: union(integer(), binary()))) ==
840+
assert union(open_map(a: binary()), dynamic(open_map(a: union(integer(), binary())))) ==
826841
open_map(a: dynamic(integer()) |> union(binary()))
827842

828843
# For domains too
@@ -833,7 +848,31 @@ defmodule Module.Types.DescrTest do
833848
# if_set on dynamic fields also must work
834849
t1 = dynamic(open_map(a: if_set(integer())))
835850
t2 = open_map(a: if_set(dynamic(integer())))
836-
assert t1 == t2
851+
assert union(open_map(a: not_set()), t1) == t2
852+
end
853+
854+
test "structural types preserve static part of gradual elements" do
855+
static = atom([:ok])
856+
gradual = union(static, dynamic(integer()))
857+
upper_bound = upper_bound(gradual)
858+
x = atom([:x])
859+
head = atom([:head])
860+
861+
for {descr, static_descr, dynamic_descr} <- [
862+
{tuple([gradual, x]), tuple([static, x]), tuple([upper_bound, x])},
863+
{open_tuple([gradual]), open_tuple([static]), open_tuple([upper_bound])},
864+
{closed_map(a: gradual), closed_map(a: static), closed_map(a: upper_bound)},
865+
{open_map(a: gradual), open_map(a: static), open_map(a: upper_bound)},
866+
{closed_map([{domain_key(:integer), gradual}]),
867+
closed_map([{domain_key(:integer), static}]),
868+
closed_map([{domain_key(:integer), upper_bound}])},
869+
{non_empty_list(gradual), non_empty_list(static), non_empty_list(upper_bound)},
870+
{non_empty_list(head, gradual), non_empty_list(head, static),
871+
non_empty_list(head, upper_bound)}
872+
] do
873+
assert descr == Map.put(static_descr, :dynamic, dynamic_descr)
874+
assert subtype?(static_descr, descr)
875+
end
837876
end
838877
end
839878

@@ -1687,6 +1726,7 @@ defmodule Module.Types.DescrTest do
16871726
assert tuple_fetch(dynamic(empty_tuple()), 0) == :badindex
16881727
assert tuple_fetch(dynamic(tuple([integer(), atom()])), 2) == :badindex
16891728
assert tuple_fetch(union(dynamic(), integer()), 0) == :badtuple
1729+
assert tuple_fetch(tuple([none()]), 0) == :badtuple
16901730

16911731
assert tuple_fetch(dynamic(tuple()), 0)
16921732
|> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end)
@@ -1752,6 +1792,7 @@ defmodule Module.Types.DescrTest do
17521792
assert tuple_insert_at(tuple([integer(), atom()]), -1, boolean()) == :badindex
17531793
assert tuple_insert_at(integer(), 0, boolean()) == :badtuple
17541794
assert tuple_insert_at(term(), 0, boolean()) == :badtuple
1795+
assert tuple_insert_at(tuple([none()]), 0, boolean()) == :badtuple
17551796

17561797
# Out-of-bounds in a union
17571798
assert union(tuple([integer(), atom()]), tuple([float()]))
@@ -3013,6 +3054,7 @@ defmodule Module.Types.DescrTest do
30133054
end
30143055

30153056
test "list" do
3057+
assert non_empty_list(none()) |> to_quoted_string() == "none()"
30163058
assert list(term()) |> to_quoted_string() == "list(term())"
30173059
assert list(integer()) |> to_quoted_string() == "list(integer())"
30183060

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ defmodule Module.Types.ExprTest do
6060

6161
assert typecheck!([:ok, 123]) == non_empty_list(union(atom([:ok]), integer()))
6262
assert typecheck!([:ok | 123]) == non_empty_list(atom([:ok]), integer())
63-
assert typecheck!([x], [:ok, x]) == dynamic(non_empty_list(term()))
63+
64+
assert typecheck!([x], [:ok, x])
65+
|> equal?(union(non_empty_list(atom([:ok])), dynamic(non_empty_list(term()))))
66+
6467
assert typecheck!([x], [:ok | x]) == dynamic(non_empty_list(term(), term()))
6568
end
6669

0 commit comments

Comments
 (0)