Skip to content

Commit 2d32724

Browse files
committed
Type Map.replace/3 and Map.replace_lazy/3
1 parent f58e02a commit 2d32724

File tree

2 files changed

+188
-34
lines changed

2 files changed

+188
-34
lines changed

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

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ defmodule Module.Types.Apply do
251251
{Map, :pop, [{[open_map(), term()], tuple([term(), open_map()])}]},
252252
{Map, :pop, [{[open_map(), term(), term()], tuple([term(), open_map()])}]},
253253
{Map, :pop!, [{[open_map(), term()], tuple([term(), open_map()])}]},
254+
{Map, :replace, [{[open_map(), term(), term()], open_map()}]},
255+
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
254256
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
255257
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
256258
{:maps, :find,
@@ -419,22 +421,22 @@ defmodule Module.Types.Apply do
419421
case map_update(map, @struct_key, not_set(), false, true) do
420422
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
421423
:badmap -> {:error, badremote(Map, :from_struct, args_types)}
422-
{:error, _errors} -> {:error, {:badkeydomain, map, @struct_key, nil}}
424+
{:error, _errors} -> {:error, {:badkeydomain, map, @struct_key, "raise"}}
423425
end
424426
end
425427

426428
defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do
427429
case map_get(map, key) do
428430
{:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)}
429-
:badmap -> {:error, badremote(:maps, :get, args_types)}
431+
:badmap -> {:error, badremote(Map, :get, args_types)}
430432
:error -> {:error, {:badkeydomain, map, key, @nil_atom}}
431433
end
432434
end
433435

434436
defp remote_apply(Map, :get, _info, [map, key, default] = args_types, stack) do
435437
case map_get(map, key) do
436438
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
437-
:badmap -> {:error, badremote(:maps, :get, args_types)}
439+
:badmap -> {:error, badremote(Map, :get, args_types)}
438440
:error -> {:error, {:badkeydomain, map, key, default}}
439441
end
440442
end
@@ -444,7 +446,7 @@ defmodule Module.Types.Apply do
444446
{:ok, default} ->
445447
case map_get(map, key) do
446448
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
447-
:badmap -> {:error, badremote(:maps, :get, args_types)}
449+
:badmap -> {:error, badremote(Map, :get_lazy, args_types)}
448450
:error -> {:error, {:badkeydomain, map, key, default}}
449451
end
450452

@@ -477,36 +479,26 @@ defmodule Module.Types.Apply do
477479
case map_update(map, key, not_set(), true, false) do
478480
{value, descr, _errors} -> {:ok, return(tuple([value, descr]), args_types, stack)}
479481
:badmap -> {:error, badremote(Map, :pop!, args_types)}
480-
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
482+
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
481483
end
482484
end
483485

484-
defp remote_apply(Map, :update!, _info, [map, key, fun] = args_types, stack) do
485-
try do
486-
{map, fun} =
487-
case fun do
488-
%{dynamic: fun} -> {dynamic(map), fun}
489-
_ -> {map, fun}
490-
end
491-
492-
fun_apply = fn arg_type ->
493-
case fun_apply(fun, [arg_type]) do
494-
{:ok, res} -> res
495-
reason -> throw({:badapply, reason, [arg_type]})
496-
end
497-
end
498-
499-
map_update_fun(map, key, fun_apply, false, false)
500-
catch
501-
{:badapply, reason, args_types} ->
502-
{:error, {:badapply, fun, args_types, reason}}
503-
else
486+
defp remote_apply(Map, :replace, _info, [map, key, value] = args_types, stack) do
487+
case map_update(map, key, value, false, false) do
504488
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
505-
:badmap -> {:error, badremote(Map, :update!, args_types)}
506-
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
489+
:badmap -> {:error, badremote(Map, :replace, args_types)}
490+
{:error, _errors} -> {:error, {:badkeydomain, map, key, "do nothing"}}
507491
end
508492
end
509493

494+
defp remote_apply(Map, :replace_lazy, _info, args_types, stack) do
495+
map_update_or_replace_lazy(:replace_lazy, args_types, stack, "do nothing")
496+
end
497+
498+
defp remote_apply(Map, :update!, _info, args_types, stack) do
499+
map_update_or_replace_lazy(:update!, args_types, stack, "raise")
500+
end
501+
510502
defp remote_apply(:maps, :find, _info, [key, map] = args_types, stack) do
511503
case map_get(map, key) do
512504
{_, value} ->
@@ -559,7 +551,7 @@ defmodule Module.Types.Apply do
559551
case map_get(map, key) do
560552
{:ok, value} -> {:ok, return(value, args_types, stack)}
561553
:badmap -> {:error, badremote(:maps, :get, args_types)}
562-
:error -> {:error, {:badkeydomain, map, key, nil}}
554+
:error -> {:error, {:badkeydomain, map, key, "raise"}}
563555
end
564556
end
565557

@@ -574,15 +566,15 @@ defmodule Module.Types.Apply do
574566
case map_update(map, key, value, false, true) do
575567
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
576568
:badmap -> {:error, badremote(:maps, :put, args_types)}
577-
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
569+
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
578570
end
579571
end
580572

581573
defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do
582574
case map_update(map, key, not_set(), false, true) do
583575
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
584576
:badmap -> {:error, badremote(:maps, :remove, args_types)}
585-
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
577+
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
586578
end
587579
end
588580

@@ -611,7 +603,7 @@ defmodule Module.Types.Apply do
611603
case map_update(map, key, value, false, false) do
612604
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
613605
:badmap -> {:error, badremote(:maps, :update, args_types)}
614-
{:error, _errors} -> {:error, {:badkeydomain, map, key, nil}}
606+
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
615607
end
616608
end
617609

@@ -975,6 +967,34 @@ defmodule Module.Types.Apply do
975967
end
976968
end
977969

970+
## Map helpers
971+
972+
def map_update_or_replace_lazy(name, [map, key, fun] = args_types, stack, error) do
973+
try do
974+
{map, fun} =
975+
case fun do
976+
%{dynamic: fun} -> {dynamic(map), fun}
977+
_ -> {map, fun}
978+
end
979+
980+
fun_apply = fn arg_type ->
981+
case fun_apply(fun, [arg_type]) do
982+
{:ok, res} -> res
983+
reason -> throw({:badapply, reason, [arg_type]})
984+
end
985+
end
986+
987+
map_update_fun(map, key, fun_apply, false, false)
988+
catch
989+
{:badapply, reason, args_types} ->
990+
{:error, {:badapply, fun, args_types, reason}}
991+
else
992+
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
993+
:badmap -> {:error, badremote(Map, name, args_types)}
994+
{:error, _errors} -> {:error, {:badkeydomain, map, key, error}}
995+
end
996+
end
997+
978998
## Application helpers
979999

9801000
defp return(type, args_types, stack) do
@@ -1257,10 +1277,10 @@ defmodule Module.Types.Apply do
12571277
12581278
#{to_quoted_string(key) |> indent(4)}
12591279
1260-
therefore this function will always #{if error do
1261-
"return #{to_quoted_string(error)}"
1280+
therefore this function will always #{if is_binary(error) do
1281+
error
12621282
else
1263-
"raise"
1283+
"return #{to_quoted_string(error)}"
12641284
end}
12651285
""",
12661286
format_traces(traces)

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

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,140 @@ defmodule Module.Types.MapTest do
756756
end
757757
end
758758

759+
describe "Map.replace/3" do
760+
test "checking" do
761+
assert typecheck!(Map.replace(%{key: 123}, :key, :value)) ==
762+
closed_map(key: atom([:value]))
763+
764+
assert typecheck!([x], Map.replace(x, :key, :value)) ==
765+
dynamic(open_map(key: atom([:value])))
766+
767+
# If one of them succeeds, we are still fine!
768+
assert typecheck!(
769+
[condition?],
770+
Map.replace(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123")
771+
) == closed_map(foo: binary())
772+
773+
assert typecheck!([x], Map.replace(x, 123, 456)) == dynamic(open_map())
774+
end
775+
776+
test "inference" do
777+
assert typecheck!(
778+
[x],
779+
(
780+
_ = Map.replace(x, :key, :value)
781+
x
782+
)
783+
) == dynamic(open_map())
784+
end
785+
786+
test "errors" do
787+
assert typeerror!(Map.replace(%{}, :key, :value)) =~
788+
"""
789+
incompatible types given to Map.replace/3:
790+
791+
Map.replace(%{}, :key, :value)
792+
793+
the map:
794+
795+
empty_map()
796+
797+
does not have all required keys:
798+
799+
:key
800+
801+
therefore this function will always do nothing
802+
"""
803+
end
804+
end
805+
806+
describe "Map.replace_lazy/3" do
807+
test "checking" do
808+
assert typecheck!(Map.replace_lazy(%{key: 123}, :key, fn _ -> :value end)) ==
809+
dynamic(closed_map(key: atom([:value])))
810+
811+
assert typecheck!([x], Map.replace_lazy(x, :key, fn _ -> :value end)) ==
812+
dynamic(open_map(key: atom([:value])))
813+
814+
# If one of them succeeds, we are still fine!
815+
assert typecheck!(
816+
[condition?],
817+
Map.replace_lazy(%{foo: 123}, if(condition?, do: :foo, else: :bar), fn _ ->
818+
"123"
819+
end)
820+
) == dynamic(closed_map(foo: binary()))
821+
822+
# Both succeed but different clauses
823+
assert typecheck!(
824+
[condition?],
825+
Map.replace_lazy(
826+
%{key1: :foo, key2: :bar},
827+
if(condition?, do: :key1, else: :key2),
828+
fn
829+
:foo -> 123
830+
:bar -> 123.0
831+
end
832+
)
833+
) ==
834+
dynamic(
835+
union(
836+
closed_map(key1: atom([:foo]), key2: float()),
837+
closed_map(key1: integer(), key2: atom([:bar]))
838+
)
839+
)
840+
841+
assert typecheck!([x], Map.replace_lazy(x, 123, fn _ -> 456 end)) == dynamic(open_map())
842+
843+
assert typecheck!([], Map.replace_lazy(%{123 => 456}, 123, fn x -> x * 1.0 end)) ==
844+
dynamic(closed_map([{domain_key(:integer), union(integer(), float())}]))
845+
end
846+
847+
test "inference" do
848+
assert typecheck!(
849+
[x],
850+
(
851+
_ = Map.replace_lazy(x, :key, fn _ -> :value end)
852+
x
853+
)
854+
) == dynamic(open_map())
855+
end
856+
857+
test "errors" do
858+
assert typeerror!(Map.replace_lazy(%{}, :key, fn _ -> :value end)) =~
859+
"""
860+
incompatible types given to Map.replace_lazy/3:
861+
862+
Map.replace_lazy(%{}, :key, fn _ -> :value end)
863+
864+
the map:
865+
866+
empty_map()
867+
868+
does not have all required keys:
869+
870+
:key
871+
872+
therefore this function will always do nothing
873+
"""
874+
875+
assert typeerror!(Map.replace_lazy(%{key: :foo}, :key, fn :bar -> :value end))
876+
|> strip_ansi() =~
877+
"""
878+
incompatible types given on function call within Map.replace_lazy/3:
879+
880+
Map.replace_lazy(%{key: :foo}, :key, fn :bar -> :value end)
881+
882+
given types:
883+
884+
dynamic(:foo)
885+
886+
but function has type:
887+
888+
(:bar -> dynamic(:value))
889+
"""
890+
end
891+
end
892+
759893
describe "Map.replace!/3" do
760894
test "checking" do
761895
assert typecheck!(Map.replace!(%{key: 123}, :key, :value)) ==

0 commit comments

Comments
 (0)