Skip to content

Commit 1aa188f

Browse files
gldubcjosevalim
authored andcommitted
Add map_to_list
1 parent 41f9bcf commit 1aa188f

2 files changed

Lines changed: 263 additions & 5 deletions

File tree

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

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ defmodule Module.Types.Descr do
5151
list: @non_empty_list_top,
5252
fun: @fun_top
5353
}
54+
@list_top %{bitmap: @bit_empty_list, list: @non_empty_list_top}
5455
@empty_list %{bitmap: @bit_empty_list}
5556
@not_non_empty_list Map.delete(@term, :list)
5657
@not_list Map.replace!(@not_non_empty_list, :bitmap, @bit_top - @bit_empty_list)
@@ -314,6 +315,9 @@ defmodule Module.Types.Descr do
314315
end
315316
end
316317

318+
defp optional_static?(%{optional: 1}), do: true
319+
defp optional_static?(%{}), do: false
320+
317321
defp pop_optional_static(:term), do: {false, :term}
318322

319323
defp pop_optional_static(type) do
@@ -2295,7 +2299,6 @@ defmodule Module.Types.Descr do
22952299

22962300
defp domain_key_to_descr(:atom), do: atom()
22972301
defp domain_key_to_descr(:binary), do: binary()
2298-
defp domain_key_to_descr(:empty_list), do: empty_list()
22992302
defp domain_key_to_descr(:integer), do: integer()
23002303
defp domain_key_to_descr(:float), do: float()
23012304
defp domain_key_to_descr(:pid), do: pid()
@@ -2304,7 +2307,7 @@ defmodule Module.Types.Descr do
23042307
defp domain_key_to_descr(:fun), do: fun()
23052308
defp domain_key_to_descr(:tuple), do: tuple()
23062309
defp domain_key_to_descr(:map), do: open_map()
2307-
defp domain_key_to_descr(:list), do: non_empty_list(term(), term())
2310+
defp domain_key_to_descr(:list), do: @list_top
23082311

23092312
defp map_descr(tag, pairs, default, force_domains?) do
23102313
{fields, domains, dynamic?} = map_descr_pairs(pairs, [], %{}, false)
@@ -2744,6 +2747,118 @@ defmodule Module.Types.Descr do
27442747
end)
27452748
end
27462749

2750+
@doc """
2751+
Returns the map converted into a list.
2752+
"""
2753+
def map_to_list(:term), do: :badmap
2754+
2755+
def map_to_list(descr) do
2756+
case :maps.take(:dynamic, descr) do
2757+
:error ->
2758+
if descr_key?(descr, :map) and map_only?(descr) do
2759+
map_to_list_static(descr.map)
2760+
else
2761+
:badmap
2762+
end
2763+
2764+
{dynamic, static} ->
2765+
if descr_key?(dynamic, :map) and map_only?(static) do
2766+
with {:ok, dynamic_type} <- map_to_list_static(dynamic.map) do
2767+
if descr_key?(static, :map) do
2768+
with {:ok, static_type} <- map_to_list_static(static.map) do
2769+
{:ok, union(static_type, dynamic(dynamic_type))}
2770+
end
2771+
else
2772+
{:ok, dynamic(dynamic_type)}
2773+
end
2774+
end
2775+
else
2776+
:badmap
2777+
end
2778+
end
2779+
end
2780+
2781+
defp map_to_list_static(bdd) do
2782+
case map_bdd_to_dnf(bdd) do
2783+
[] ->
2784+
:badmap
2785+
2786+
dnf ->
2787+
case map_to_list_static(bdd, dnf) do
2788+
value when value == @none ->
2789+
{:ok, empty_list()}
2790+
2791+
inner ->
2792+
if has_empty_map?(dnf) do
2793+
{:ok, list(inner)}
2794+
else
2795+
{:ok, non_empty_list(inner)}
2796+
end
2797+
end
2798+
end
2799+
end
2800+
2801+
defp has_empty_map?(dnf) do
2802+
Enum.any?(dnf, fn {_, fields, negs} ->
2803+
Enum.all?(fields, fn {_key, value} -> optional_static?(value) end) and
2804+
Enum.all?(negs, fn {_, fields} ->
2805+
not Enum.all?(fields, fn {_key, value} -> optional_static?(value) end)
2806+
end)
2807+
end)
2808+
end
2809+
2810+
defp map_to_list_static(bdd, dnf) do
2811+
try do
2812+
# Check if any line in the DNF represents an open map or compute the union of domain keys types
2813+
Enum.reduce(dnf, none(), fn
2814+
{tag_or_domains, _fields, _negs}, acc ->
2815+
case tag_or_domains do
2816+
:open ->
2817+
# A negation cannot make an open map closed without cancelling it completely,
2818+
# which is filtered by `map_bdd_to_dnf/1`.
2819+
throw(:open)
2820+
2821+
domains = %{} ->
2822+
Enum.reduce(domains, acc, fn {domain_key, value}, acc ->
2823+
value = remove_optional(value)
2824+
2825+
if empty?(value) do
2826+
acc
2827+
else
2828+
union(acc, tuple([domain_key_to_descr(domain_key), value]))
2829+
end
2830+
end)
2831+
2832+
_ ->
2833+
acc
2834+
end
2835+
end)
2836+
catch
2837+
:open -> tuple([term(), term()])
2838+
else
2839+
domain_keys_type ->
2840+
{_seen, acc} =
2841+
bdd_reduce(bdd, {%{}, domain_keys_type}, fn {_tag, fields}, seen_acc ->
2842+
Enum.reduce(fields, seen_acc, fn {key, _type}, {seen, acc} ->
2843+
if Map.has_key?(seen, key) do
2844+
{seen, acc}
2845+
else
2846+
value = dnf |> map_dnf_fetch_static(key) |> remove_optional_static()
2847+
seen = Map.put(seen, key, [])
2848+
2849+
if empty?(value) do
2850+
{seen, acc}
2851+
else
2852+
{seen, union(acc, tuple([atom([key]), value]))}
2853+
end
2854+
end
2855+
end)
2856+
end)
2857+
2858+
acc
2859+
end
2860+
end
2861+
27472862
@doc """
27482863
Updates `key_descr` in `descr` with `type`.
27492864
@@ -2979,7 +3094,7 @@ defmodule Module.Types.Descr do
29793094
if Map.has_key?(seen, key) do
29803095
{seen, acc}
29813096
else
2982-
value = dnf |> map_dnf_fetch_static(key) |> remove_optional()
3097+
value = dnf |> map_dnf_fetch_static(key) |> remove_optional_static()
29833098
{Map.put(seen, key, []), union(acc, value)}
29843099
end
29853100
end)
@@ -3994,8 +4109,8 @@ defmodule Module.Types.Descr do
39944109
here_branch ++ later_branches
39954110
end
39964111

3997-
# No more negative elements to process: there is no all-equal branch to add,
3998-
# because were constructing {t} ant not {u}, which must differ somewhere.
4112+
# No more negative elements to process: there is no "all-equal" branch to add,
4113+
# because we're constructing {t} ant not {u}, which must differ somewhere.
39994114
defp tuple_elim_content(_acc, _tag, _elements, []) do
40004115
[]
40014116
end

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

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,139 @@ defmodule Module.Types.DescrTest do
15091509
|> equal?(integer())
15101510
end
15111511

1512+
test "map_to_list" do
1513+
assert map_to_list(:term) == :badmap
1514+
assert map_to_list(integer()) == :badmap
1515+
assert map_to_list(union(open_map(), integer())) == :badmap
1516+
assert map_to_list(none()) == :badmap
1517+
1518+
# A non existent map type is refused
1519+
assert open_map()
1520+
|> difference(open_map(a: if_set(term()), c: if_set(term())))
1521+
|> map_to_list() == :badmap
1522+
1523+
assert map_to_list(empty_map()) == {:ok, empty_list()}
1524+
assert map_to_list(open_map()) == {:ok, list(tuple([term(), term()]))}
1525+
1526+
assert map_to_list(closed_map(a: integer())) ==
1527+
{:ok, non_empty_list(tuple([atom([:a]), integer()]))}
1528+
1529+
assert map_to_list(closed_map(a: integer(), b: atom())) ==
1530+
{:ok,
1531+
non_empty_list(
1532+
tuple([atom([:a]), integer()])
1533+
|> union(tuple([atom([:b]), atom()]))
1534+
)}
1535+
1536+
assert map_to_list(union(closed_map(a: float()), closed_map(b: pid()))) ==
1537+
{:ok,
1538+
non_empty_list(
1539+
tuple([atom([:a]), float()])
1540+
|> union(tuple([atom([:b]), pid()]))
1541+
)}
1542+
1543+
# Test with domain keys
1544+
assert map_to_list(closed_map([{domain_key(:integer), binary()}])) ==
1545+
{:ok, list(tuple([integer(), binary()]))}
1546+
1547+
assert map_to_list(closed_map([{domain_key(:tuple), binary()}])) ==
1548+
{:ok, list(tuple([tuple(), binary()]))}
1549+
1550+
# Test with both atom keys and domain keys
1551+
map_with_both =
1552+
closed_map([
1553+
{:a, atom([:ok])},
1554+
{:b, float()},
1555+
{domain_key(:integer), binary()},
1556+
{domain_key(:tuple), pid()}
1557+
])
1558+
1559+
assert map_to_list(map_with_both) ==
1560+
{:ok,
1561+
non_empty_list(
1562+
tuple([atom([:a]), atom([:ok])])
1563+
|> union(tuple([atom([:b]), float()]))
1564+
|> union(tuple([integer(), binary()]))
1565+
|> union(tuple([tuple(), pid()]))
1566+
)}
1567+
1568+
# Test open maps - should return list of key-value tuples
1569+
assert map_to_list(open_map()) == {:ok, list(tuple([term(), term()]))}
1570+
assert map_to_list(open_map(a: integer())) == {:ok, non_empty_list(tuple([term(), term()]))}
1571+
1572+
{:ok, list} = map_to_list(open_map([{domain_key(:integer), binary()}]))
1573+
1574+
assert list(
1575+
Enum.reduce(
1576+
[binary(), float(), pid(), port(), reference()] ++
1577+
[fun(), atom(), tuple(), open_map(), list(term(), term())],
1578+
tuple([integer(), binary()]),
1579+
fn domain, acc -> union(acc, tuple([domain, term()])) end
1580+
)
1581+
)
1582+
|> equal?(list)
1583+
1584+
# Test with multiple domain keys
1585+
multiple_domains =
1586+
closed_map([
1587+
{domain_key(:integer), atom([:int])},
1588+
{domain_key(:float), atom([:float])},
1589+
{domain_key(:atom), binary()},
1590+
{domain_key(:binary), integer()},
1591+
{domain_key(:tuple), float()}
1592+
])
1593+
1594+
assert map_to_list(multiple_domains) ==
1595+
{:ok,
1596+
list(
1597+
tuple([integer(), atom([:int])])
1598+
|> union(tuple([float(), atom([:float])]))
1599+
|> union(tuple([atom(), binary()]))
1600+
|> union(tuple([binary(), integer()]))
1601+
|> union(tuple([tuple(), float()]))
1602+
)}
1603+
1604+
# Test dynamic maps
1605+
assert map_to_list(dynamic(open_map())) ==
1606+
{:ok, dynamic(list(tuple([term(), term()])))}
1607+
1608+
assert map_to_list(dynamic(closed_map(a: integer()))) ==
1609+
{:ok, dynamic(non_empty_list(tuple([atom([:a]), integer()])))}
1610+
1611+
assert map_to_list(union(dynamic(closed_map(a: integer())), closed_map(b: atom()))) ==
1612+
{:ok,
1613+
union(
1614+
non_empty_list(tuple([atom([:b]), atom()])),
1615+
dynamic(
1616+
non_empty_list(
1617+
union(
1618+
tuple([atom([:a]), integer()]),
1619+
tuple([atom([:b]), atom()])
1620+
)
1621+
)
1622+
)
1623+
)}
1624+
1625+
# A static integer is refused
1626+
assert map_to_list(union(dynamic(open_map()), integer())) == :badmap
1627+
1628+
# Test with negations
1629+
assert map_to_list(
1630+
difference(closed_map(a: integer(), b: atom()), closed_map(a: integer()))
1631+
) ==
1632+
{:ok,
1633+
non_empty_list(
1634+
tuple([atom([:a]), integer()])
1635+
|> union(tuple([atom([:b]), atom()]))
1636+
)}
1637+
1638+
# If a key is removed entirely by a negation, it should not appear in the result
1639+
assert closed_map(a: if_set(integer()), b: atom())
1640+
|> difference(closed_map(a: integer(), b: term()))
1641+
|> map_to_list() ==
1642+
{:ok, non_empty_list(tuple([atom([:b]), atom()]))}
1643+
end
1644+
15121645
test "domain_to_args" do
15131646
# take complex tuples, normalize them, and check if they are still equal
15141647
complex_tuples = [
@@ -1803,6 +1936,11 @@ defmodule Module.Types.DescrTest do
18031936
{type, descr, []} = map_update(dynamic(open_map()), atom([:key1, :key2]), integer())
18041937
assert equal?(type, dynamic())
18051938
assert descr == dynamic(union(open_map(key1: integer()), open_map(key2: integer())))
1939+
1940+
# A non-existing map
1941+
assert open_map()
1942+
|> difference(open_map(a: if_set(term()), c: if_set(term())))
1943+
|> map_update(atom([:b]), integer()) == {:error, [badkey: :b]}
18061944
end
18071945

18081946
test "with dynamic atom keys" do
@@ -1889,6 +2027,11 @@ defmodule Module.Types.DescrTest do
18892027
assert map_update(closed_map(key1: binary(), key2: pid()), dynamic(atom()), integer()) ==
18902028
{union(binary(), pid()),
18912029
closed_map(key1: union(integer(), binary()), key2: union(integer(), pid())), []}
2030+
2031+
# A non-existing map
2032+
assert open_map()
2033+
|> difference(open_map(a: if_set(term()), c: if_set(term())))
2034+
|> map_update(binary(), integer()) == {:error, [baddomain: binary()]}
18922035
end
18932036

18942037
test "with mixed keys" do

0 commit comments

Comments
 (0)