Skip to content

Commit b60e424

Browse files
committed
Typecheck Map.from_struct/1
1 parent 70e23a4 commit b60e424

File tree

4 files changed

+59
-6
lines changed

4 files changed

+59
-6
lines changed

lib/elixir/lib/map.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,7 @@ defmodule Map do
10881088
#=> %{name: "john"}
10891089
10901090
"""
1091+
# TODO: implement this using row polymorphism
10911092
@spec from_struct(atom | struct) :: map
10921093
def from_struct(struct) when is_atom(struct) do
10931094
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,15 @@ defmodule Module.Types.Apply do
244244
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]},
245245

246246
## Map
247-
{:maps, :remove, [{[term(), open_map()], open_map()}]},
247+
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
248248
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
249249
{:maps, :find,
250250
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
251251
{:maps, :get, [{[term(), open_map()], term()}]},
252252
{:maps, :is_key, [{[term(), open_map()], boolean()}]},
253253
{:maps, :keys, [{[open_map()], dynamic(list(term()))}]},
254254
{:maps, :put, [{[term(), term(), open_map()], open_map()}]},
255+
{:maps, :remove, [{[term(), open_map()], open_map()}]},
255256
{:maps, :take,
256257
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
257258
{:maps, :to_list, [{[open_map()], dynamic(list(tuple([term(), term()])))}]},
@@ -427,11 +428,13 @@ defmodule Module.Types.Apply do
427428
end
428429
end
429430

430-
defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do
431-
case map_update(map, key, not_set(), false, true) do
431+
@struct_key atom([:__struct__])
432+
433+
defp remote_apply(Map, :from_struct, _info, [map] = args_types, stack) do
434+
case map_update(map, @struct_key, not_set(), false, true) do
432435
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
433-
:badmap -> {:error, badremote(:maps, :remove, 2)}
434-
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
436+
:badmap -> {:error, badremote(Map, :from_struct, 1)}
437+
{:error, _errors} -> {:error, {:badkeydomain, map, @struct_key, nil}}
435438
end
436439
end
437440

@@ -472,6 +475,14 @@ defmodule Module.Types.Apply do
472475
end
473476
end
474477

478+
defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do
479+
case map_update(map, key, not_set(), false, true) do
480+
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
481+
:badmap -> {:error, badremote(:maps, :remove, 2)}
482+
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
483+
end
484+
end
485+
475486
defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do
476487
case map_update(map, key, not_set(), true, false) do
477488
# We could suggest to use :maps.delete if the key always exists

lib/elixir/src/elixir_compiler.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ bootstrap_files() ->
196196
[
197197
<<"list/chars.ex">>,
198198
<<"bitwise.ex">>,
199+
<<"map.ex">>,
199200
<<"module/parallel_checker.ex">>,
200201
<<"module/behaviour.ex">>,
201202
<<"module/types/helpers.ex">>,
@@ -208,7 +209,6 @@ bootstrap_files() ->
208209
<<"exception.ex">>,
209210
<<"path.ex">>,
210211
<<"file.ex">>,
211-
<<"map.ex">>,
212212
<<"access.ex">>,
213213
<<"io.ex">>,
214214
<<"system.ex">>,

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,47 @@ defmodule Module.Types.MapTest do
249249
end
250250
end
251251

252+
describe "Map.from_struct/1" do
253+
test "checking" do
254+
assert typecheck!(Map.from_struct(%{})) ==
255+
closed_map(__struct__: not_set())
256+
257+
assert typecheck!(Map.from_struct(%{key: 123})) ==
258+
closed_map(key: integer(), __struct__: not_set())
259+
260+
assert typecheck!(Map.from_struct(%URI{})) ==
261+
closed_map(
262+
__struct__: not_set(),
263+
authority: atom([nil]),
264+
fragment: atom([nil]),
265+
host: atom([nil]),
266+
path: atom([nil]),
267+
port: atom([nil]),
268+
query: atom([nil]),
269+
scheme: atom([nil]),
270+
userinfo: atom([nil])
271+
)
272+
273+
assert typecheck!([x], Map.from_struct(x)) ==
274+
dynamic(open_map(__struct__: not_set()))
275+
end
276+
277+
test "inference" do
278+
assert typecheck!(
279+
[x],
280+
(
281+
_ = Map.from_struct(x)
282+
x
283+
)
284+
) == dynamic(open_map())
285+
end
286+
287+
test "errors" do
288+
assert typeerror!([x = []], Map.from_struct(x)) =~
289+
"incompatible types given to Map.from_struct/1"
290+
end
291+
end
292+
252293
describe "Map.put/3" do
253294
test "checking" do
254295
assert typecheck!(Map.put(%{}, :key, :value)) ==

0 commit comments

Comments
 (0)