Skip to content

Commit 030660a

Browse files
committed
Type Map.to_list/1, Map.keys/1, and Map.values/1
1 parent fc07f45 commit 030660a

File tree

4 files changed

+251
-37
lines changed

4 files changed

+251
-37
lines changed

lib/elixir/lib/module/types/apply.ex

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,12 @@ defmodule Module.Types.Apply do
241241
{:erlang, :send, [{[send_destination, term()], dynamic()}]},
242242
{:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
243243
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
244-
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]}
244+
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
245+
246+
## Map
247+
{:maps, :keys, [{[open_map()], dynamic(list(term()))}]},
248+
{:maps, :to_list, [{[open_map()], dynamic(list(tuple([term(), term()])))}]},
249+
{:maps, :values, [{[open_map()], dynamic(list(term()))}]}
245250
] do
246251
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
247252

@@ -314,14 +319,6 @@ defmodule Module.Types.Apply do
314319
{{:make_tuple, size}, [integer(), term()], context}
315320
end
316321

317-
def remote_domain(:erlang, :hd, [_list], expected, _meta, _stack, context) do
318-
{:hd, [non_empty_list(expected, term())], context}
319-
end
320-
321-
def remote_domain(:erlang, :tl, [_list], _expected, _meta, _stack, context) do
322-
{:tl, [non_empty_list(term(), term())], context}
323-
end
324-
325322
def remote_domain(:erlang, name, [_left, _right], _expected, _meta, stack, context)
326323
when name in [:>=, :"=<", :>, :<, :min, :max] do
327324
{{:ordered_compare, name, is_warning(stack)}, [term(), term()], context}
@@ -350,7 +347,7 @@ defmodule Module.Types.Apply do
350347
Applies a previously collected domain from `remote_domain/7`.
351348
"""
352349
def remote_apply(info, mod, fun, args_types, expr, stack, context) do
353-
case remote_apply(info, args_types, stack) do
350+
case remote_apply(mod, fun, info, args_types, stack) do
354351
{:ok, type} ->
355352
{type, context}
356353

@@ -361,6 +358,45 @@ defmodule Module.Types.Apply do
361358
end
362359
end
363360

361+
defp remote_apply(:erlang, :hd, _info, [list], _stack) do
362+
case list_hd(list) do
363+
{_, value_type} -> {:ok, value_type}
364+
:badnonemptylist -> {:error, badremote(:erlang, :hd, 1)}
365+
end
366+
end
367+
368+
defp remote_apply(:erlang, :tl, _info, [list], _stack) do
369+
case list_tl(list) do
370+
{_, value_type} -> {:ok, value_type}
371+
:badnonemptylist -> {:error, badremote(:erlang, :tl, 1)}
372+
end
373+
end
374+
375+
defp remote_apply(:maps, :keys, _info, [map], _stack) do
376+
case map_to_list(map, fn key, _value -> key end) do
377+
{:ok, list_type} -> {:ok, list_type}
378+
:badmap -> {:error, badremote(:maps, :keys, 1)}
379+
end
380+
end
381+
382+
defp remote_apply(:maps, :values, _info, [map], _stack) do
383+
case map_to_list(map, fn _key, value -> value end) do
384+
{:ok, list_type} -> {:ok, list_type}
385+
:badmap -> {:error, badremote(:maps, :keys, 1)}
386+
end
387+
end
388+
389+
defp remote_apply(:maps, :to_list, _info, [map], _stack) do
390+
case map_to_list(map) do
391+
{:ok, list_type} -> {:ok, list_type}
392+
:badmap -> {:error, badremote(:maps, :to_list, 1)}
393+
end
394+
end
395+
396+
defp remote_apply(_mod, _fun, info, args_types, stack) do
397+
remote_apply(info, args_types, stack)
398+
end
399+
364400
defp remote_apply(:none, _args_types, _stack) do
365401
{:ok, dynamic()}
366402
end
@@ -407,20 +443,6 @@ defmodule Module.Types.Apply do
407443
{:ok, tuple(List.duplicate(elem, size))}
408444
end
409445

410-
defp remote_apply(:hd, [list], _stack) do
411-
case list_hd(list) do
412-
{_, value_type} -> {:ok, value_type}
413-
:badnonemptylist -> {:error, badremote(:erlang, :hd, 1)}
414-
end
415-
end
416-
417-
defp remote_apply(:tl, [list], _stack) do
418-
case list_tl(list) do
419-
{_, value_type} -> {:ok, value_type}
420-
:badnonemptylist -> {:error, badremote(:erlang, :tl, 1)}
421-
end
422-
end
423-
424446
defp remote_apply({:ordered_compare, name, check?}, [left, right], stack) do
425447
result =
426448
if name in [:min, :max] do

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

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2750,22 +2750,24 @@ defmodule Module.Types.Descr do
27502750
@doc """
27512751
Returns the map converted into a list.
27522752
"""
2753-
def map_to_list(:term), do: :badmap
2753+
def map_to_list(descr, fun \\ &tuple([&1, &2]))
27542754

2755-
def map_to_list(descr) do
2755+
def map_to_list(:term, _fun), do: :badmap
2756+
2757+
def map_to_list(descr, fun) do
27562758
case :maps.take(:dynamic, descr) do
27572759
:error ->
2758-
if descr_key?(descr, :map) and map_only?(descr) do
2759-
map_to_list_static(descr.map)
2760+
if map_only?(descr) do
2761+
map_to_list_static(descr, fun)
27602762
else
27612763
:badmap
27622764
end
27632765

27642766
{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 map_only?(static) do
2768+
with {:ok, dynamic_type} <- map_to_list_static(dynamic, fun) do
27672769
if descr_key?(static, :map) do
2768-
with {:ok, static_type} <- map_to_list_static(static.map) do
2770+
with {:ok, static_type} <- map_to_list_static(static, fun) do
27692771
{:ok, union(static_type, dynamic(dynamic_type))}
27702772
end
27712773
else
@@ -2778,13 +2780,13 @@ defmodule Module.Types.Descr do
27782780
end
27792781
end
27802782

2781-
defp map_to_list_static(bdd) do
2783+
defp map_to_list_static(%{map: bdd}, fun) do
27822784
case map_bdd_to_dnf(bdd) do
27832785
[] ->
27842786
:badmap
27852787

27862788
dnf ->
2787-
case map_to_list_static(bdd, dnf) do
2789+
case map_to_list_static(bdd, dnf, fun) do
27882790
value when value == @none ->
27892791
{:ok, empty_list()}
27902792

@@ -2798,6 +2800,14 @@ defmodule Module.Types.Descr do
27982800
end
27992801
end
28002802

2803+
defp map_to_list_static(%{}, _fun) do
2804+
:badmap
2805+
end
2806+
2807+
defp map_to_list_static(:term, fun) do
2808+
{:ok, list(fun.(term(), term()))}
2809+
end
2810+
28012811
defp has_empty_map?(dnf) do
28022812
Enum.any?(dnf, fn {_, fields, negs} ->
28032813
Enum.all?(fields, fn {_key, value} -> optional_static?(value) end) and
@@ -2807,7 +2817,7 @@ defmodule Module.Types.Descr do
28072817
end)
28082818
end
28092819

2810-
defp map_to_list_static(bdd, dnf) do
2820+
defp map_to_list_static(bdd, dnf, fun) do
28112821
try do
28122822
# Check if any line in the DNF represents an open map or compute the union of domain keys types
28132823
Enum.reduce(dnf, none(), fn
@@ -2825,7 +2835,7 @@ defmodule Module.Types.Descr do
28252835
if empty?(value) do
28262836
acc
28272837
else
2828-
union(acc, tuple([domain_key_to_descr(domain_key), value]))
2838+
union(acc, fun.(domain_key_to_descr(domain_key), value))
28292839
end
28302840
end)
28312841

@@ -2834,7 +2844,7 @@ defmodule Module.Types.Descr do
28342844
end
28352845
end)
28362846
catch
2837-
:open -> tuple([term(), term()])
2847+
:open -> fun.(term(), term())
28382848
else
28392849
domain_keys_type ->
28402850
{_seen, acc} =
@@ -2849,7 +2859,7 @@ defmodule Module.Types.Descr do
28492859
if empty?(value) do
28502860
{seen, acc}
28512861
else
2852-
{seen, union(acc, tuple([atom([key]), value]))}
2862+
{seen, union(acc, fun.(atom([key]), value))}
28532863
end
28542864
end
28552865
end)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,7 @@ defmodule Module.Types.DescrTest do
15141514
assert map_to_list(integer()) == :badmap
15151515
assert map_to_list(union(open_map(), integer())) == :badmap
15161516
assert map_to_list(none()) == :badmap
1517+
assert map_to_list(dynamic()) == {:ok, dynamic(list(tuple([term(), term()])))}
15171518

15181519
# A non existent map type is refused
15191520
assert open_map()
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: 2021 The Elixir Team
3+
# SPDX-FileCopyrightText: 2012 Plataformatec
4+
5+
Code.require_file("type_helper.exs", __DIR__)
6+
7+
defmodule Module.Types.MapTest do
8+
# Tests for the Map module
9+
use ExUnit.Case, async: true
10+
11+
import TypeHelper
12+
import Module.Types.Descr
13+
defmacro domain_key(arg) when is_atom(arg), do: [arg]
14+
15+
describe "Map.to_list/1" do
16+
test "checking" do
17+
assert typecheck!([x = %{}], Map.to_list(x)) == dynamic(list(tuple([term(), term()])))
18+
19+
assert typecheck!(
20+
(
21+
x = %{}
22+
Map.to_list(x)
23+
)
24+
) == empty_list()
25+
26+
assert typecheck!(
27+
(
28+
x = %{"c" => :three}
29+
Map.to_list(x)
30+
)
31+
) ==
32+
list(tuple([binary(), atom([:three])]))
33+
34+
assert typecheck!(
35+
(
36+
x = %{a: 1, b: "two"}
37+
Map.to_list(x)
38+
)
39+
) ==
40+
non_empty_list(
41+
union(tuple([atom([:a]), integer()]), tuple([atom([:b]), binary()]))
42+
)
43+
44+
assert typecheck!(
45+
(
46+
x = %{"c" => :three, a: 1, b: "two"}
47+
Map.to_list(x)
48+
)
49+
) ==
50+
non_empty_list(
51+
tuple([atom([:a]), integer()])
52+
|> union(tuple([atom([:b]), binary()]))
53+
|> union(tuple([binary(), atom([:three])]))
54+
)
55+
end
56+
57+
test "inference" do
58+
assert typecheck!(
59+
[x],
60+
(
61+
_ = Map.to_list(x)
62+
x
63+
)
64+
) == dynamic(open_map())
65+
end
66+
67+
test "errors" do
68+
assert typeerror!([x = []], Map.to_list(x)) =~ "incompatible types given to Map.to_list/1"
69+
end
70+
end
71+
72+
describe "Map.keys/1" do
73+
test "checking" do
74+
assert typecheck!([x = %{}], Map.keys(x)) == dynamic(list(term()))
75+
76+
assert typecheck!(
77+
(
78+
x = %{}
79+
Map.keys(x)
80+
)
81+
) == empty_list()
82+
83+
assert typecheck!(
84+
(
85+
x = %{"c" => :three}
86+
Map.keys(x)
87+
)
88+
) ==
89+
list(binary())
90+
91+
assert typecheck!(
92+
(
93+
x = %{a: 1, b: "two"}
94+
Map.keys(x)
95+
)
96+
) ==
97+
non_empty_list(union(atom([:a]), atom([:b])))
98+
99+
assert typecheck!(
100+
(
101+
x = %{"c" => :three, a: 1, b: "two"}
102+
Map.keys(x)
103+
)
104+
) ==
105+
non_empty_list(
106+
atom([:a])
107+
|> union(atom([:b]))
108+
|> union(binary())
109+
)
110+
end
111+
112+
test "inference" do
113+
assert typecheck!(
114+
[x],
115+
(
116+
_ = Map.keys(x)
117+
x
118+
)
119+
) == dynamic(open_map())
120+
end
121+
122+
test "errors" do
123+
assert typeerror!([x = []], Map.keys(x)) =~ "incompatible types given to Map.keys/1"
124+
end
125+
end
126+
127+
describe "Map.values/1" do
128+
test "checking" do
129+
assert typecheck!([x = %{}], Map.values(x)) == dynamic(list(term()))
130+
131+
assert typecheck!(
132+
(
133+
x = %{}
134+
Map.values(x)
135+
)
136+
) == empty_list()
137+
138+
assert typecheck!(
139+
(
140+
x = %{"c" => :three}
141+
Map.values(x)
142+
)
143+
) ==
144+
list(atom([:three]))
145+
146+
assert typecheck!(
147+
(
148+
x = %{a: 1, b: "two"}
149+
Map.values(x)
150+
)
151+
) ==
152+
non_empty_list(union(integer(), binary()))
153+
154+
assert typecheck!(
155+
(
156+
x = %{"c" => :three, a: 1, b: "two"}
157+
Map.values(x)
158+
)
159+
) ==
160+
non_empty_list(
161+
integer()
162+
|> union(binary())
163+
|> union(atom([:three]))
164+
)
165+
end
166+
167+
test "inference" do
168+
assert typecheck!(
169+
[x],
170+
(
171+
_ = Map.values(x)
172+
x
173+
)
174+
) == dynamic(open_map())
175+
end
176+
177+
test "errors" do
178+
assert typeerror!([x = []], Map.values(x)) =~ "incompatible types given to Map.values/1"
179+
end
180+
end
181+
end

0 commit comments

Comments
 (0)