Skip to content
Draft
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
217 changes: 152 additions & 65 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2130,9 +2130,31 @@ defmodule Module.Types.Descr do
#
# none() types can be given and, while stored, it means the list type is empty.
defp list_descr(list_type, last_type, empty?) do
{list_dynamic?, list_type} = list_pop_dynamic(list_type)
{last_dynamic?, last_type} = list_pop_dynamic(last_type)
{dynamic_list_type, static_list_type} = pop_dynamic(list_type)
{dynamic_last_type, static_last_type} = pop_dynamic(last_type)
dynamic? = dynamic_list_type != static_list_type or dynamic_last_type != static_last_type
static_possible? = not empty?(static_list_type) and not empty?(static_last_type)

dynamic_descr = list_descr_build(dynamic_list_type, dynamic_last_type, empty?)

cond do
empty?(dynamic_descr) ->
%{}

dynamic? ->
if not static_possible? do
%{dynamic: dynamic_descr}
else
static_descr = list_descr_build(static_list_type, static_last_type, empty?)
Map.put(static_descr, :dynamic, dynamic_descr)
end

true ->
dynamic_descr
end
end

defp list_descr_build(list_type, last_type, empty?) do
list_part =
if last_type == :term do
list_new(:term, :term)
Expand Down Expand Up @@ -2163,13 +2185,7 @@ defmodule Module.Types.Descr do
end
end

list_descr =
if empty?, do: %{list: list_part, bitmap: @bit_empty_list}, else: %{list: list_part}

case list_dynamic? or last_dynamic? do
true -> %{dynamic: list_descr}
false -> list_descr
end
if empty?, do: %{list: list_part, bitmap: @bit_empty_list}, else: %{list: list_part}
end

defp list_new(list_type, last_type), do: bdd_leaf(list_type, last_type)
Expand Down Expand Up @@ -2216,15 +2232,6 @@ defmodule Module.Types.Descr do
end)
end

defp list_pop_dynamic(:term), do: {false, :term}

defp list_pop_dynamic(descr) do
case :maps.take(:dynamic, descr) do
:error -> {false, descr}
{dynamic, _} -> {true, dynamic}
end
end

defp list_tail_unfold(:term), do: @not_non_empty_list
defp list_tail_unfold(other), do: Map.delete(other, :list)

Expand Down Expand Up @@ -2772,8 +2779,29 @@ defmodule Module.Types.Descr do
defp domain_key_to_descr(:list), do: @list_top

defp map_descr(tag, pairs) do
{fields, domains, dynamic?} = map_descr_pairs(pairs, [], @fields_new, false)
{fields, domains, dynamic_fields, dynamic_domains, dynamic?, static_possible?} =
map_descr_pairs(pairs, [], @fields_new, [], @fields_new, false, true)

dynamic_descr = map_descr_build(tag, dynamic_fields, dynamic_domains)

cond do
empty?(dynamic_descr) ->
%{}

dynamic? ->
if not static_possible? do
%{dynamic: dynamic_descr}
else
static_descr = map_descr_build(tag, fields, domains)
Map.put(static_descr, :dynamic, dynamic_descr)
end

true ->
dynamic_descr
end
end

defp map_descr_build(tag, fields, domains) do
map_new =
if not is_fields_empty(domains) do
domains =
Expand All @@ -2789,10 +2817,7 @@ defmodule Module.Types.Descr do
map_new(tag, fields)
end

case dynamic? do
true -> %{dynamic: %{map: map_new}}
false -> %{map: map_new}
end
%{map: map_new}
end

defp map_put_domain(domain, domain_keys, value) when is_list(domain_keys) do
Expand All @@ -2813,28 +2838,89 @@ defmodule Module.Types.Descr do

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

defp map_descr_pairs([{key, :term} | rest], fields, domain, dynamic?) do
defp map_descr_pairs(
[{key, :term} | rest],
fields,
domain,
dynamic_fields,
dynamic_domain,
dynamic?,
static_possible?
) do
case is_atom(key) do
true -> map_descr_pairs(rest, [{key, :term} | fields], domain, dynamic?)
false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, :term), dynamic?)
true ->
map_descr_pairs(
rest,
[{key, :term} | fields],
domain,
[{key, :term} | dynamic_fields],
dynamic_domain,
dynamic?,
static_possible?
)

false ->
map_descr_pairs(
rest,
fields,
map_put_domain(domain, key, :term),
dynamic_fields,
map_put_domain(dynamic_domain, key, :term),
dynamic?,
static_possible?
)
end
end

defp map_descr_pairs([{key, value} | rest], fields, domain, dynamic?) do
{value, dynamic?} =
case :maps.take(:dynamic, value) do
:error -> {value, dynamic?}
{dynamic, _static} -> {dynamic, true}
end
defp map_descr_pairs(
[{key, value} | rest],
fields,
domain,
dynamic_fields,
dynamic_domain,
dynamic?,
static_possible?
) do
{dynamic_value, static_value} = pop_dynamic(value)
dynamic? = dynamic? or dynamic_value != static_value
static_possible? = static_possible? and not empty?(static_value)

case is_atom(key) do
true -> map_descr_pairs(rest, [{key, value} | fields], domain, dynamic?)
false -> map_descr_pairs(rest, fields, map_put_domain(domain, key, value), dynamic?)
true ->
map_descr_pairs(
rest,
[{key, static_value} | fields],
domain,
[{key, dynamic_value} | dynamic_fields],
dynamic_domain,
dynamic?,
static_possible?
)

false ->
map_descr_pairs(
rest,
fields,
map_put_domain(domain, key, static_value),
dynamic_fields,
map_put_domain(dynamic_domain, key, dynamic_value),
dynamic?,
static_possible?
)
end
end

defp map_descr_pairs([], fields, domain, dynamic?) do
{fields_from_reverse_list(fields), domain, dynamic?}
defp map_descr_pairs(
[],
fields,
domain,
dynamic_fields,
dynamic_domain,
dynamic?,
static_possible?
) do
{fields_from_reverse_list(fields), domain, fields_from_reverse_list(dynamic_fields),
dynamic_domain, dynamic?, static_possible?}
end

# Gets the default type associated to atom keys in a map.
Expand Down Expand Up @@ -4901,46 +4987,47 @@ defmodule Module.Types.Descr do
# - {atom(), boolean(), ...} is encoded as {:open, [atom(), boolean()]}

defp tuple_descr(tag, fields) do
case tuple_descr(fields, [], false) do
:empty -> %{}
{fields, true} -> %{dynamic: %{tuple: tuple_new(tag, Enum.reverse(fields))}}
{_, false} -> %{tuple: tuple_new(tag, fields)}
case tuple_descr(fields, [], [], false, true) do
:empty ->
%{}

{static_fields, dynamic_fields, true, static_possible?} ->
dynamic_descr = %{tuple: tuple_new(tag, :lists.reverse(dynamic_fields))}

if not static_possible? do
%{dynamic: dynamic_descr}
else
static_descr = %{tuple: tuple_new(tag, :lists.reverse(static_fields))}
Map.put(static_descr, :dynamic, dynamic_descr)
end

{fields, _dynamic_fields, false, _static_possible?} ->
%{tuple: tuple_new(tag, :lists.reverse(fields))}
end
end

defp tuple_descr([:term | rest], acc, dynamic?) do
tuple_descr(rest, [:term | acc], dynamic?)
defp tuple_descr([:term | rest], acc, dynamic_acc, dynamic?, static_possible?) do
tuple_descr(rest, [:term | acc], [:term | dynamic_acc], dynamic?, static_possible?)
end

defp tuple_descr([value | rest], acc, dynamic?) do
# Check if the static part is empty
static_empty? =
case value do
# Has dynamic component, check static separately
%{dynamic: _} -> false
_ -> empty?(value)
end
defp tuple_descr([value | rest], acc, dynamic_acc, dynamic?, static_possible?) do
{dynamic_value, static_value} = pop_dynamic(value)

if static_empty? do
if empty?(dynamic_value) do
:empty
else
case :maps.take(:dynamic, value) do
:error ->
tuple_descr(rest, [value | acc], dynamic?)

{dynamic, _static} ->
# Check if dynamic component is empty
if empty?(dynamic) do
:empty
else
tuple_descr(rest, [dynamic | acc], true)
end
end
tuple_descr(
rest,
[static_value | acc],
[dynamic_value | dynamic_acc],
dynamic? or dynamic_value != static_value,
static_possible? and not empty?(static_value)
)
end
end

defp tuple_descr([], acc, dynamic?) do
{acc, dynamic?}
defp tuple_descr([], acc, dynamic_acc, dynamic?, static_possible?) do
{acc, dynamic_acc, dynamic?, static_possible?}
end

defp tuple_new(tag, elements), do: bdd_leaf(tag, elements)
Expand Down
37 changes: 35 additions & 2 deletions lib/elixir/test/elixir/module/types/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ defmodule Module.Types.DescrTest do
test "map hoists dynamic" do
assert dynamic(open_map(a: integer())) == open_map(a: dynamic(integer()))

assert dynamic(open_map(a: union(integer(), binary()))) ==
assert union(open_map(a: binary()), dynamic(open_map(a: union(integer(), binary())))) ==
open_map(a: dynamic(integer()) |> union(binary()))

# For domains too
Expand All @@ -798,7 +798,40 @@ defmodule Module.Types.DescrTest do
# if_set on dynamic fields also must work
t1 = dynamic(open_map(a: if_set(integer())))
t2 = open_map(a: if_set(dynamic(integer())))
assert t1 == t2
assert union(open_map(a: not_set()), t1) == t2
end

test "tuple preserves static part of gradual elements" do
x = union(atom([:ok]), dynamic(integer()))

assert tuple([x, atom([:x])]) == %{
tuple: {:closed, [atom([:ok]), atom([:x])]},
dynamic: %{tuple: {:closed, [upper_bound(x), atom([:x])]}}
}

assert subtype?(tuple([atom([:ok]), atom([:x])]), tuple([x, atom([:x])]))
end

test "closed_map preserves static part of gradual values" do
x = union(atom([:ok]), dynamic(integer()))

assert closed_map(a: x) == %{
map: {:closed, [a: atom([:ok])]},
dynamic: %{map: {:closed, [a: upper_bound(x)]}}
}

assert subtype?(closed_map(a: atom([:ok])), closed_map(a: x))
end

test "non_empty_list preserves static part of gradual head types" do
x = union(atom([:ok]), dynamic(integer()))

assert non_empty_list(x) == %{
list: {atom([:ok]), empty_list()},
dynamic: %{list: {upper_bound(x), empty_list()}}
}

assert subtype?(non_empty_list(atom([:ok])), non_empty_list(x))
end
end

Expand Down
5 changes: 4 additions & 1 deletion lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ defmodule Module.Types.ExprTest do

assert typecheck!([:ok, 123]) == non_empty_list(union(atom([:ok]), integer()))
assert typecheck!([:ok | 123]) == non_empty_list(atom([:ok]), integer())
assert typecheck!([x], [:ok, x]) == dynamic(non_empty_list(term()))

assert typecheck!([x], [:ok, x])
|> equal?(union(non_empty_list(atom([:ok])), dynamic(non_empty_list(term()))))

assert typecheck!([x], [:ok | x]) == dynamic(non_empty_list(term(), term()))
end

Expand Down
21 changes: 16 additions & 5 deletions lib/elixir/test/elixir/module/types/map_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,10 @@ defmodule Module.Types.MapTest do
describe "Map.pop_lazy/3" do
test "checking" do
assert typecheck!(Map.pop_lazy(%{key: 123}, :key, fn -> :error end)) ==
dynamic(tuple([union(integer(), atom([:error])), empty_map()]))
union(
tuple([integer(), empty_map()]),
dynamic(tuple([union(integer(), atom([:error])), empty_map()]))
)

assert typecheck!([x], Map.pop_lazy(x, :key, fn -> :error end)) ==
dynamic(tuple([term(), open_map(key: not_set())]))
Expand All @@ -626,13 +629,21 @@ defmodule Module.Types.MapTest do
x = %{String.to_integer(x) => :before}
Map.pop_lazy(x, 123, fn -> :after end)
)
) ==
dynamic(
)
|> equal?(
union(
tuple([
atom([:before, :after]),
atom([:before]),
closed_map([{domain_key(:integer), atom([:before])}])
])
]),
dynamic(
tuple([
atom([:before, :after]),
closed_map([{domain_key(:integer), atom([:before])}])
])
)
)
)
end

test "inference" do
Expand Down
Loading