Skip to content

Commit 0f3fdcd

Browse files
committed
Type Map.put_new/3 and Map.put_new_lazy/3
1 parent 2d32724 commit 0f3fdcd

File tree

3 files changed

+169
-17
lines changed

3 files changed

+169
-17
lines changed

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

Lines changed: 30 additions & 4 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, :put_new, [{[open_map(), term(), term()], open_map()}]},
255+
{Map, :put_new_lazy, [{[open_map(), term(), fun(0)], open_map()}]},
254256
{Map, :replace, [{[open_map(), term(), term()], open_map()}]},
255257
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
256258
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
@@ -421,7 +423,7 @@ defmodule Module.Types.Apply do
421423
case map_update(map, @struct_key, not_set(), false, true) do
422424
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
423425
:badmap -> {:error, badremote(Map, :from_struct, args_types)}
424-
{:error, _errors} -> {:error, {:badkeydomain, map, @struct_key, "raise"}}
426+
{:error, _errors} -> {:ok, map}
425427
end
426428
end
427429

@@ -455,6 +457,17 @@ defmodule Module.Types.Apply do
455457
end
456458
end
457459

460+
defp remote_apply(Map, :put_new, _info, [map, key, value] = args_types, stack) do
461+
map_put_new(map, key, value, :put_new, args_types, stack)
462+
end
463+
464+
defp remote_apply(Map, :put_new_lazy, _info, [map, key, fun] = args_types, stack) do
465+
case fun_apply(fun, []) do
466+
{:ok, value} -> map_put_new(map, key, value, :put_new_lazy, args_types, stack)
467+
reason -> {:error, {:badapply, fun, [], reason}}
468+
end
469+
end
470+
458471
defp remote_apply(Map, :pop, _info, args_types, stack) do
459472
[map, key, default] =
460473
case args_types do
@@ -566,15 +579,15 @@ defmodule Module.Types.Apply do
566579
case map_update(map, key, value, false, true) do
567580
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
568581
:badmap -> {:error, badremote(:maps, :put, args_types)}
569-
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
582+
{:error, _errors} -> {:ok, map}
570583
end
571584
end
572585

573586
defp remote_apply(:maps, :remove, _info, [key, map] = args_types, stack) do
574587
case map_update(map, key, not_set(), false, true) do
575588
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
576589
:badmap -> {:error, badremote(:maps, :remove, args_types)}
577-
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
590+
{:error, _errors} -> {:ok, map}
578591
end
579592
end
580593

@@ -969,6 +982,19 @@ defmodule Module.Types.Apply do
969982

970983
## Map helpers
971984

985+
defp map_put_new(map, key, value, name, args_types, stack) do
986+
fun = fn
987+
true, type -> union(type, value)
988+
false, type -> if empty?(type), do: value, else: type
989+
end
990+
991+
case map_update_fun(map, key, fun, false, true) do
992+
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
993+
:badmap -> {:error, badremote(Map, name, args_types)}
994+
{:error, _errors} -> {:ok, map}
995+
end
996+
end
997+
972998
def map_update_or_replace_lazy(name, [map, key, fun] = args_types, stack, error) do
973999
try do
9741000
{map, fun} =
@@ -977,7 +1003,7 @@ defmodule Module.Types.Apply do
9771003
_ -> {map, fun}
9781004
end
9791005

980-
fun_apply = fn arg_type ->
1006+
fun_apply = fn _optional?, arg_type ->
9811007
case fun_apply(fun, [arg_type]) do
9821008
{:ok, res} -> res
9831009
reason -> throw({:badapply, reason, [arg_type]})

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2999,10 +2999,12 @@ defmodule Module.Types.Descr do
29992999
def map_update(descr, key_descr, type, return_type?, force?) do
30003000
case type do
30013001
%{dynamic: dynamic} ->
3002-
map_update_unchecked(dynamic(descr), key_descr, fn _ -> dynamic end, return_type?, force?)
3002+
fun = fn _, _ -> dynamic end
3003+
map_update_unchecked(dynamic(descr), key_descr, fun, return_type?, force?)
30033004

30043005
%{} ->
3005-
map_update_unchecked(descr, key_descr, fn _ -> type end, return_type?, force?)
3006+
fun = fn _, _ -> type end
3007+
map_update_unchecked(descr, key_descr, fun, return_type?, force?)
30063008
end
30073009
end
30083010

@@ -3020,12 +3022,19 @@ defmodule Module.Types.Descr do
30203022
def map_update_fun(descr, key_descr, type_fun, return_type? \\ true, force? \\ false) do
30213023
gradual? = gradual?(descr)
30223024

3023-
type_fun = fn value ->
3024-
value = remove_optional(value)
3025+
type_fun = fn optional?, value ->
3026+
if is_function(type_fun, 1) do
3027+
case type_fun.(if gradual?, do: dynamic(value), else: value) do
3028+
%{dynamic: dynamic} -> dynamic
3029+
descr -> descr
3030+
end
3031+
else
3032+
value = if gradual?, do: dynamic(value), else: value
30253033

3026-
case type_fun.(if gradual?, do: dynamic(value), else: value) do
3027-
%{dynamic: dynamic} -> dynamic
3028-
descr -> descr
3034+
case type_fun.(optional?, value) do
3035+
%{dynamic: dynamic} -> dynamic
3036+
descr -> descr
3037+
end
30293038
end
30303039
end
30313040

@@ -3176,7 +3185,7 @@ defmodule Module.Types.Descr do
31763185
{acc_value, acc_descr, acc_errors, acc_found?}
31773186
else
31783187
acc_value = union(value, acc_value)
3179-
acc_descr = union(map_put_key_static(descr, key, type_fun.(value)), acc_descr)
3188+
acc_descr = union(map_put_key_static(descr, key, type_fun.(optional?, value)), acc_descr)
31803189

31813190
# The field will be missing if we are not forcing,
31823191
# we are in static mode and the value is optional.
@@ -3253,7 +3262,8 @@ defmodule Module.Types.Descr do
32533262
if :sets.is_element(key, negated) do
32543263
{key, value}
32553264
else
3256-
{key, union(value, type_fun.(value))}
3265+
{optional?, call_value} = pop_optional_static(value)
3266+
{key, union(value, type_fun.(optional?, call_value))}
32573267
end
32583268
end)
32593269

@@ -3364,13 +3374,16 @@ defmodule Module.Types.Descr do
33643374
:open
33653375

33663376
:closed ->
3367-
Map.from_keys(domain_keys, if_set(type_fun.(none())))
3377+
Map.from_keys(domain_keys, if_set(type_fun.(true, none())))
33683378

33693379
domains = %{} ->
33703380
Enum.reduce(domain_keys, domains, fn domain_key, acc ->
33713381
case acc do
3372-
%{^domain_key => value} -> %{acc | domain_key => union(value, type_fun.(value))}
3373-
%{} -> Map.put(acc, domain_key, if_set(type_fun.(none())))
3382+
%{^domain_key => value} ->
3383+
%{acc | domain_key => union(value, type_fun.(true, remove_optional(value)))}
3384+
3385+
%{} ->
3386+
Map.put(acc, domain_key, if_set(type_fun.(true, none())))
33743387
end
33753388
end)
33763389
end
@@ -3438,7 +3451,7 @@ defmodule Module.Types.Descr do
34383451
{required_keys, optional_keys, maybe_negated_set, required_domains, optional_domains} =
34393452
split_keys
34403453

3441-
type_fun = fn _ -> type end
3454+
type_fun = fn _, _ -> type end
34423455
bdd = map_update_put_negated(bdd, maybe_negated_set, type_fun)
34433456

34443457
descr =

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

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

759+
describe "Map.put_new_lazy/3" do
760+
test "checking" do
761+
assert typecheck!(Map.put_new_lazy(%{}, :key, fn -> :value end)) ==
762+
closed_map(key: atom([:value]))
763+
764+
assert typecheck!(Map.put_new_lazy(%{key: 123}, :key, fn -> :value end)) ==
765+
closed_map(key: integer())
766+
767+
assert typecheck!([x], Map.put_new_lazy(x, :key, fn -> :value end)) ==
768+
dynamic(open_map(key: term()))
769+
770+
# If one of them succeeds, we are still fine!
771+
assert typecheck!(
772+
[condition?],
773+
Map.put_new_lazy(%{foo: 123}, if(condition?, do: :foo, else: :bar), fn -> "123" end)
774+
) == union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary()))
775+
776+
assert typecheck!([], Map.put_new_lazy(%{789 => "binary"}, 123, fn -> 456 end)) ==
777+
closed_map([{domain_key(:integer), union(binary(), integer())}])
778+
779+
assert typecheck!([x], Map.put_new_lazy(x, 123, fn -> 456 end)) == dynamic(open_map())
780+
end
781+
782+
test "inference" do
783+
assert typecheck!(
784+
[x],
785+
(
786+
_ = Map.put_new_lazy(x, :key, fn -> :value end)
787+
x
788+
)
789+
) == dynamic(open_map())
790+
end
791+
792+
test "errors" do
793+
assert typeerror!([x = []], Map.put_new_lazy(x, :key, fn -> :value end)) |> strip_ansi() =~
794+
"""
795+
incompatible types given to Map.put_new_lazy/3:
796+
797+
Map.put_new_lazy(x, :key, fn -> :value end)
798+
799+
given types:
800+
801+
empty_list(), :key, (-> dynamic(:value))
802+
803+
but expected one of:
804+
805+
map(), term(), (-> term())
806+
"""
807+
808+
assert typeerror!(Map.put_new_lazy(%{}, :foo, 123)) =~
809+
"""
810+
expected a 0-arity function on function call within Map.put_new_lazy/3:
811+
812+
Map.put_new_lazy(%{}, :foo, 123)
813+
814+
but got type:
815+
816+
integer()
817+
"""
818+
end
819+
end
820+
821+
describe "Map.put_new/3" do
822+
test "checking" do
823+
assert typecheck!(Map.put_new(%{}, :key, :value)) ==
824+
closed_map(key: atom([:value]))
825+
826+
assert typecheck!(Map.put_new(%{key: 123}, :key, :value)) ==
827+
closed_map(key: integer())
828+
829+
assert typecheck!([x], Map.put_new(x, :key, :value)) ==
830+
dynamic(open_map(key: term()))
831+
832+
# If one of them succeeds, we are still fine!
833+
assert typecheck!(
834+
[condition?],
835+
Map.put_new(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123")
836+
) == union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary()))
837+
838+
assert typecheck!([], Map.put_new(%{789 => "binary"}, 123, 456)) ==
839+
closed_map([{domain_key(:integer), union(binary(), integer())}])
840+
841+
assert typecheck!([x], Map.put_new(x, 123, 456)) == dynamic(open_map())
842+
end
843+
844+
test "inference" do
845+
assert typecheck!(
846+
[x],
847+
(
848+
_ = Map.put_new(x, :key, :value)
849+
x
850+
)
851+
) == dynamic(open_map())
852+
end
853+
854+
test "errors" do
855+
assert typeerror!([x = []], Map.put_new(x, :key, :value)) |> strip_ansi() =~
856+
"""
857+
incompatible types given to Map.put_new/3:
858+
859+
Map.put_new(x, :key, :value)
860+
861+
given types:
862+
863+
empty_list(), :key, :value
864+
865+
but expected one of:
866+
867+
map(), term(), term()
868+
"""
869+
end
870+
end
871+
759872
describe "Map.replace/3" do
760873
test "checking" do
761874
assert typecheck!(Map.replace(%{key: 123}, :key, :value)) ==

0 commit comments

Comments
 (0)