Skip to content

Commit b5608f5

Browse files
lukaszsamsongldubc
authored andcommitted
Preserve static part on dynamic container
1 parent 35eeab3 commit b5608f5

4 files changed

Lines changed: 207 additions & 73 deletions

File tree

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

Lines changed: 152 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,9 +2143,31 @@ 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_list_type, static_list_type} = pop_dynamic(list_type)
2147+
{dynamic_last_type, static_last_type} = pop_dynamic(last_type)
2148+
dynamic? = dynamic_list_type != static_list_type or dynamic_last_type != static_last_type
2149+
static_possible? = not empty?(static_list_type) and not empty?(static_last_type)
21482150

2151+
dynamic_descr = list_descr_build(dynamic_list_type, dynamic_last_type, empty?)
2152+
2153+
cond do
2154+
empty?(dynamic_descr) ->
2155+
%{}
2156+
2157+
dynamic? ->
2158+
if not static_possible? do
2159+
%{dynamic: dynamic_descr}
2160+
else
2161+
static_descr = list_descr_build(static_list_type, static_last_type, empty?)
2162+
Map.put(static_descr, :dynamic, dynamic_descr)
2163+
end
2164+
2165+
true ->
2166+
dynamic_descr
2167+
end
2168+
end
2169+
2170+
defp list_descr_build(list_type, last_type, empty?) do
21492171
list_part =
21502172
if last_type == :term do
21512173
list_new(:term, :term)
@@ -2176,13 +2198,7 @@ defmodule Module.Types.Descr do
21762198
end
21772199
end
21782200

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
2201+
if empty?, do: %{list: list_part, bitmap: @bit_empty_list}, else: %{list: list_part}
21862202
end
21872203

21882204
defp list_new(list_type, last_type), do: bdd_leaf_new(list_type, last_type)
@@ -2231,15 +2247,6 @@ defmodule Module.Types.Descr do
22312247
end)
22322248
end
22332249

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-
22432250
defp list_tail_unfold(:term), do: @not_non_empty_list
22442251
defp list_tail_unfold(other), do: Map.delete(other, :list)
22452252

@@ -2784,8 +2791,29 @@ defmodule Module.Types.Descr do
27842791
defp domain_key_to_descr(:list), do: @list_top
27852792

27862793
defp map_descr(tag, pairs) do
2787-
{fields, domains, dynamic?} = map_descr_pairs(pairs, [], @fields_new, false)
2794+
{fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_possible?} =
2795+
map_descr_pairs(pairs, [], @fields_new, [], @fields_new, false, true)
2796+
2797+
dynamic_descr = map_descr_build(tag, dynamic_fields, dynamic_domains)
2798+
2799+
cond do
2800+
empty?(dynamic_descr) ->
2801+
%{}
2802+
2803+
dynamic? ->
2804+
if not static_possible? do
2805+
%{dynamic: dynamic_descr}
2806+
else
2807+
static_descr = map_descr_build(tag, fields, domains)
2808+
Map.put(static_descr, :dynamic, dynamic_descr)
2809+
end
2810+
2811+
true ->
2812+
dynamic_descr
2813+
end
2814+
end
27882815

2816+
defp map_descr_build(tag, fields, domains) do
27892817
map_new =
27902818
if not is_fields_empty(domains) do
27912819
domains =
@@ -2801,10 +2829,7 @@ defmodule Module.Types.Descr do
28012829
map_new(tag, fields)
28022830
end
28032831

2804-
case dynamic? do
2805-
true -> %{dynamic: %{map: map_new}}
2806-
false -> %{map: map_new}
2807-
end
2832+
%{map: map_new}
28082833
end
28092834

28102835
defp map_put_domain(domain, domain_keys, value) when is_list(domain_keys) do
@@ -2825,28 +2850,89 @@ defmodule Module.Types.Descr do
28252850

28262851
defp map_put_domain(domain, [], _initial, _value), do: domain
28272852

2828-
defp map_descr_pairs([{key, :term} | rest], fields, domain, dynamic?) do
2853+
defp map_descr_pairs(
2854+
[{key, :term} | rest],
2855+
fields,
2856+
domain,
2857+
dynamic_fields,
2858+
dynamic_domain,
2859+
dynamic?,
2860+
static_possible?
2861+
) do
28292862
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?)
2863+
true ->
2864+
map_descr_pairs(
2865+
rest,
2866+
[{key, :term} | fields],
2867+
domain,
2868+
[{key, :term} | dynamic_fields],
2869+
dynamic_domain,
2870+
dynamic?,
2871+
static_possible?
2872+
)
2873+
2874+
false ->
2875+
map_descr_pairs(
2876+
rest,
2877+
fields,
2878+
map_put_domain(domain, key, :term),
2879+
dynamic_fields,
2880+
map_put_domain(dynamic_domain, key, :term),
2881+
dynamic?,
2882+
static_possible?
2883+
)
28322884
end
28332885
end
28342886

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
2887+
defp map_descr_pairs(
2888+
[{key, value} | rest],
2889+
fields,
2890+
domain,
2891+
dynamic_fields,
2892+
dynamic_domain,
2893+
dynamic?,
2894+
static_possible?
2895+
) do
2896+
{dynamic_value, static_value} = pop_dynamic(value)
2897+
dynamic? = dynamic? or dynamic_value != static_value
2898+
static_possible? = static_possible? and not empty?(static_value)
28412899

28422900
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?)
2901+
true ->
2902+
map_descr_pairs(
2903+
rest,
2904+
[{key, static_value} | fields],
2905+
domain,
2906+
[{key, dynamic_value} | dynamic_fields],
2907+
dynamic_domain,
2908+
dynamic?,
2909+
static_possible?
2910+
)
2911+
2912+
false ->
2913+
map_descr_pairs(
2914+
rest,
2915+
fields,
2916+
map_put_domain(domain, key, static_value),
2917+
dynamic_fields,
2918+
map_put_domain(dynamic_domain, key, dynamic_value),
2919+
dynamic?,
2920+
static_possible?
2921+
)
28452922
end
28462923
end
28472924

2848-
defp map_descr_pairs([], fields, domain, dynamic?) do
2849-
{fields_from_reverse_list(fields), domain, dynamic?}
2925+
defp map_descr_pairs(
2926+
[],
2927+
fields,
2928+
domain,
2929+
dynamic_fields,
2930+
dynamic_domain,
2931+
dynamic?,
2932+
static_possible?
2933+
) do
2934+
{fields_from_reverse_list(fields), domain, fields_from_reverse_list(dynamic_fields),
2935+
dynamic_domain, dynamic?, static_possible?}
28502936
end
28512937

28522938
# Gets the default type associated to atom keys in a map.
@@ -4895,46 +4981,47 @@ defmodule Module.Types.Descr do
48954981
# - {atom(), boolean(), ...} is encoded as {:open, [atom(), boolean()]}
48964982

48974983
defp tuple_descr(tag, fields) do
4898-
case tuple_descr(fields, [], false) do
4899-
:empty -> %{}
4900-
{fields, true} -> %{dynamic: %{tuple: tuple_new(tag, Enum.reverse(fields))}}
4901-
{_, false} -> %{tuple: tuple_new(tag, fields)}
4984+
case tuple_descr(fields, [], [], false, true) do
4985+
:empty ->
4986+
%{}
4987+
4988+
{static_fields, dynamic_fields, true, static_possible?} ->
4989+
dynamic_descr = %{tuple: tuple_new(tag, :lists.reverse(dynamic_fields))}
4990+
4991+
if not static_possible? do
4992+
%{dynamic: dynamic_descr}
4993+
else
4994+
static_descr = %{tuple: tuple_new(tag, :lists.reverse(static_fields))}
4995+
Map.put(static_descr, :dynamic, dynamic_descr)
4996+
end
4997+
4998+
{fields, _dynamic_fields, false, _static_possible?} ->
4999+
%{tuple: tuple_new(tag, :lists.reverse(fields))}
49025000
end
49035001
end
49045002

4905-
defp tuple_descr([:term | rest], acc, dynamic?) do
4906-
tuple_descr(rest, [:term | acc], dynamic?)
5003+
defp tuple_descr([:term | rest], acc, dynamic_acc, dynamic?, static_possible?) do
5004+
tuple_descr(rest, [:term | acc], [:term | dynamic_acc], dynamic?, static_possible?)
49075005
end
49085006

4909-
defp tuple_descr([value | rest], acc, dynamic?) do
4910-
# Check if the static part is empty
4911-
static_empty? =
4912-
case value do
4913-
# Has dynamic component, check static separately
4914-
%{dynamic: _} -> false
4915-
_ -> empty?(value)
4916-
end
5007+
defp tuple_descr([value | rest], acc, dynamic_acc, dynamic?, static_possible?) do
5008+
{dynamic_value, static_value} = pop_dynamic(value)
49175009

4918-
if static_empty? do
5010+
if empty?(dynamic_value) do
49195011
:empty
49205012
else
4921-
case :maps.take(:dynamic, value) do
4922-
:error ->
4923-
tuple_descr(rest, [value | acc], dynamic?)
4924-
4925-
{dynamic, _static} ->
4926-
# Check if dynamic component is empty
4927-
if empty?(dynamic) do
4928-
:empty
4929-
else
4930-
tuple_descr(rest, [dynamic | acc], true)
4931-
end
4932-
end
5013+
tuple_descr(
5014+
rest,
5015+
[static_value | acc],
5016+
[dynamic_value | dynamic_acc],
5017+
dynamic? or dynamic_value != static_value,
5018+
static_possible? and not empty?(static_value)
5019+
)
49335020
end
49345021
end
49355022

4936-
defp tuple_descr([], acc, dynamic?) do
4937-
{acc, dynamic?}
5023+
defp tuple_descr([], acc, dynamic_acc, dynamic?, static_possible?) do
5024+
{acc, dynamic_acc, dynamic?, static_possible?}
49385025
end
49395026

49405027
defp tuple_new(tag, elements), do: bdd_leaf_new(tag, elements)

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,7 @@ defmodule Module.Types.DescrTest do
810810
test "map hoists dynamic" do
811811
assert dynamic(open_map(a: integer())) == open_map(a: dynamic(integer()))
812812

813-
assert dynamic(open_map(a: union(integer(), binary()))) ==
813+
assert union(open_map(a: binary()), dynamic(open_map(a: union(integer(), binary())))) ==
814814
open_map(a: dynamic(integer()) |> union(binary()))
815815

816816
# For domains too
@@ -821,7 +821,40 @@ defmodule Module.Types.DescrTest do
821821
# if_set on dynamic fields also must work
822822
t1 = dynamic(open_map(a: if_set(integer())))
823823
t2 = open_map(a: if_set(dynamic(integer())))
824-
assert t1 == t2
824+
assert union(open_map(a: not_set()), t1) == t2
825+
end
826+
827+
test "tuple preserves static part of gradual elements" do
828+
x = union(atom([:ok]), dynamic(integer()))
829+
830+
assert tuple([x, atom([:x])]) == %{
831+
tuple: {:closed, [atom([:ok]), atom([:x])]},
832+
dynamic: %{tuple: {:closed, [upper_bound(x), atom([:x])]}}
833+
}
834+
835+
assert subtype?(tuple([atom([:ok]), atom([:x])]), tuple([x, atom([:x])]))
836+
end
837+
838+
test "closed_map preserves static part of gradual values" do
839+
x = union(atom([:ok]), dynamic(integer()))
840+
841+
assert closed_map(a: x) == %{
842+
map: {:closed, [a: atom([:ok])]},
843+
dynamic: %{map: {:closed, [a: upper_bound(x)]}}
844+
}
845+
846+
assert subtype?(closed_map(a: atom([:ok])), closed_map(a: x))
847+
end
848+
849+
test "non_empty_list preserves static part of gradual head types" do
850+
x = union(atom([:ok]), dynamic(integer()))
851+
852+
assert non_empty_list(x) == %{
853+
list: {atom([:ok]), empty_list()},
854+
dynamic: %{list: {upper_bound(x), empty_list()}}
855+
}
856+
857+
assert subtype?(non_empty_list(atom([:ok])), non_empty_list(x))
825858
end
826859
end
827860

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

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,10 @@ defmodule Module.Types.MapTest do
601601
describe "Map.pop_lazy/3" do
602602
test "checking" do
603603
assert typecheck!(Map.pop_lazy(%{key: 123}, :key, fn -> :error end)) ==
604-
dynamic(tuple([union(integer(), atom([:error])), empty_map()]))
604+
union(
605+
tuple([integer(), empty_map()]),
606+
dynamic(tuple([union(integer(), atom([:error])), empty_map()]))
607+
)
605608

606609
assert typecheck!([x], Map.pop_lazy(x, :key, fn -> :error end)) ==
607610
dynamic(tuple([term(), open_map(key: not_set())]))
@@ -626,13 +629,21 @@ defmodule Module.Types.MapTest do
626629
x = %{String.to_integer(x) => :before}
627630
Map.pop_lazy(x, 123, fn -> :after end)
628631
)
629-
) ==
630-
dynamic(
632+
)
633+
|> equal?(
634+
union(
631635
tuple([
632-
atom([:before, :after]),
636+
atom([:before]),
633637
closed_map([{domain_key(:integer), atom([:before])}])
634-
])
638+
]),
639+
dynamic(
640+
tuple([
641+
atom([:before, :after]),
642+
closed_map([{domain_key(:integer), atom([:before])}])
643+
])
644+
)
635645
)
646+
)
636647
end
637648

638649
test "inference" do

0 commit comments

Comments
 (0)