Type Map.put_new/3 and Map.put_new_lazy/3

This commit is contained in:
José Valim
2025-12-15 11:03:52 +01:00
parent 2d327241de
commit 0f3fdcd84f
3 changed files with 169 additions and 17 deletions
+30 -4
View File
@@ -251,6 +251,8 @@ defmodule Module.Types.Apply do
{Map, :pop, [{[open_map(), term()], tuple([term(), open_map()])}]},
{Map, :pop, [{[open_map(), term(), term()], tuple([term(), open_map()])}]},
{Map, :pop!, [{[open_map(), term()], tuple([term(), open_map()])}]},
{Map, :put_new, [{[open_map(), term(), term()], open_map()}]},
{Map, :put_new_lazy, [{[open_map(), term(), fun(0)], open_map()}]},
{Map, :replace, [{[open_map(), term(), term()], open_map()}]},
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
@@ -421,7 +423,7 @@ defmodule Module.Types.Apply do
case map_update(map, @struct_key, not_set(), false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, :from_struct, args_types)}
{:error, _errors} -> {:error, {:badkeydomain, map, @struct_key, "raise"}}
{:error, _errors} -> {:ok, map}
end
end
@@ -455,6 +457,17 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(Map, :put_new, _info, [map, key, value] = args_types, stack) do
map_put_new(map, key, value, :put_new, args_types, stack)
end
defp remote_apply(Map, :put_new_lazy, _info, [map, key, fun] = args_types, stack) do
case fun_apply(fun, []) do
{:ok, value} -> map_put_new(map, key, value, :put_new_lazy, args_types, stack)
reason -> {:error, {:badapply, fun, [], reason}}
end
end
defp remote_apply(Map, :pop, _info, args_types, stack) do
[map, key, default] =
case args_types do
@@ -566,7 +579,7 @@ defmodule Module.Types.Apply do
case map_update(map, key, value, false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(:maps, :put, args_types)}
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
{:error, _errors} -> {:ok, map}
end
end
@@ -574,7 +587,7 @@ defmodule Module.Types.Apply do
case map_update(map, key, not_set(), false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(:maps, :remove, args_types)}
{:error, _errors} -> {:error, {:badkeydomain, map, key, "raise"}}
{:error, _errors} -> {:ok, map}
end
end
@@ -969,6 +982,19 @@ defmodule Module.Types.Apply do
## Map helpers
defp map_put_new(map, key, value, name, args_types, stack) do
fun = fn
true, type -> union(type, value)
false, type -> if empty?(type), do: value, else: type
end
case map_update_fun(map, key, fun, false, true) do
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
:badmap -> {:error, badremote(Map, name, args_types)}
{:error, _errors} -> {:ok, map}
end
end
def map_update_or_replace_lazy(name, [map, key, fun] = args_types, stack, error) do
try do
{map, fun} =
@@ -977,7 +1003,7 @@ defmodule Module.Types.Apply do
_ -> {map, fun}
end
fun_apply = fn arg_type ->
fun_apply = fn _optional?, arg_type ->
case fun_apply(fun, [arg_type]) do
{:ok, res} -> res
reason -> throw({:badapply, reason, [arg_type]})
+26 -13
View File
@@ -2999,10 +2999,12 @@ defmodule Module.Types.Descr do
def map_update(descr, key_descr, type, return_type?, force?) do
case type do
%{dynamic: dynamic} ->
map_update_unchecked(dynamic(descr), key_descr, fn _ -> dynamic end, return_type?, force?)
fun = fn _, _ -> dynamic end
map_update_unchecked(dynamic(descr), key_descr, fun, return_type?, force?)
%{} ->
map_update_unchecked(descr, key_descr, fn _ -> type end, return_type?, force?)
fun = fn _, _ -> type end
map_update_unchecked(descr, key_descr, fun, return_type?, force?)
end
end
@@ -3020,12 +3022,19 @@ defmodule Module.Types.Descr do
def map_update_fun(descr, key_descr, type_fun, return_type? \\ true, force? \\ false) do
gradual? = gradual?(descr)
type_fun = fn value ->
value = remove_optional(value)
type_fun = fn optional?, value ->
if is_function(type_fun, 1) do
case type_fun.(if gradual?, do: dynamic(value), else: value) do
%{dynamic: dynamic} -> dynamic
descr -> descr
end
else
value = if gradual?, do: dynamic(value), else: value
case type_fun.(if gradual?, do: dynamic(value), else: value) do
%{dynamic: dynamic} -> dynamic
descr -> descr
case type_fun.(optional?, value) do
%{dynamic: dynamic} -> dynamic
descr -> descr
end
end
end
@@ -3176,7 +3185,7 @@ defmodule Module.Types.Descr do
{acc_value, acc_descr, acc_errors, acc_found?}
else
acc_value = union(value, acc_value)
acc_descr = union(map_put_key_static(descr, key, type_fun.(value)), acc_descr)
acc_descr = union(map_put_key_static(descr, key, type_fun.(optional?, value)), acc_descr)
# The field will be missing if we are not forcing,
# we are in static mode and the value is optional.
@@ -3253,7 +3262,8 @@ defmodule Module.Types.Descr do
if :sets.is_element(key, negated) do
{key, value}
else
{key, union(value, type_fun.(value))}
{optional?, call_value} = pop_optional_static(value)
{key, union(value, type_fun.(optional?, call_value))}
end
end)
@@ -3364,13 +3374,16 @@ defmodule Module.Types.Descr do
:open
:closed ->
Map.from_keys(domain_keys, if_set(type_fun.(none())))
Map.from_keys(domain_keys, if_set(type_fun.(true, none())))
domains = %{} ->
Enum.reduce(domain_keys, domains, fn domain_key, acc ->
case acc do
%{^domain_key => value} -> %{acc | domain_key => union(value, type_fun.(value))}
%{} -> Map.put(acc, domain_key, if_set(type_fun.(none())))
%{^domain_key => value} ->
%{acc | domain_key => union(value, type_fun.(true, remove_optional(value)))}
%{} ->
Map.put(acc, domain_key, if_set(type_fun.(true, none())))
end
end)
end
@@ -3438,7 +3451,7 @@ defmodule Module.Types.Descr do
{required_keys, optional_keys, maybe_negated_set, required_domains, optional_domains} =
split_keys
type_fun = fn _ -> type end
type_fun = fn _, _ -> type end
bdd = map_update_put_negated(bdd, maybe_negated_set, type_fun)
descr =
@@ -756,6 +756,119 @@ defmodule Module.Types.MapTest do
end
end
describe "Map.put_new_lazy/3" do
test "checking" do
assert typecheck!(Map.put_new_lazy(%{}, :key, fn -> :value end)) ==
closed_map(key: atom([:value]))
assert typecheck!(Map.put_new_lazy(%{key: 123}, :key, fn -> :value end)) ==
closed_map(key: integer())
assert typecheck!([x], Map.put_new_lazy(x, :key, fn -> :value end)) ==
dynamic(open_map(key: term()))
# If one of them succeeds, we are still fine!
assert typecheck!(
[condition?],
Map.put_new_lazy(%{foo: 123}, if(condition?, do: :foo, else: :bar), fn -> "123" end)
) == union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary()))
assert typecheck!([], Map.put_new_lazy(%{789 => "binary"}, 123, fn -> 456 end)) ==
closed_map([{domain_key(:integer), union(binary(), integer())}])
assert typecheck!([x], Map.put_new_lazy(x, 123, fn -> 456 end)) == dynamic(open_map())
end
test "inference" do
assert typecheck!(
[x],
(
_ = Map.put_new_lazy(x, :key, fn -> :value end)
x
)
) == dynamic(open_map())
end
test "errors" do
assert typeerror!([x = []], Map.put_new_lazy(x, :key, fn -> :value end)) |> strip_ansi() =~
"""
incompatible types given to Map.put_new_lazy/3:
Map.put_new_lazy(x, :key, fn -> :value end)
given types:
empty_list(), :key, (-> dynamic(:value))
but expected one of:
map(), term(), (-> term())
"""
assert typeerror!(Map.put_new_lazy(%{}, :foo, 123)) =~
"""
expected a 0-arity function on function call within Map.put_new_lazy/3:
Map.put_new_lazy(%{}, :foo, 123)
but got type:
integer()
"""
end
end
describe "Map.put_new/3" do
test "checking" do
assert typecheck!(Map.put_new(%{}, :key, :value)) ==
closed_map(key: atom([:value]))
assert typecheck!(Map.put_new(%{key: 123}, :key, :value)) ==
closed_map(key: integer())
assert typecheck!([x], Map.put_new(x, :key, :value)) ==
dynamic(open_map(key: term()))
# If one of them succeeds, we are still fine!
assert typecheck!(
[condition?],
Map.put_new(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123")
) == union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary()))
assert typecheck!([], Map.put_new(%{789 => "binary"}, 123, 456)) ==
closed_map([{domain_key(:integer), union(binary(), integer())}])
assert typecheck!([x], Map.put_new(x, 123, 456)) == dynamic(open_map())
end
test "inference" do
assert typecheck!(
[x],
(
_ = Map.put_new(x, :key, :value)
x
)
) == dynamic(open_map())
end
test "errors" do
assert typeerror!([x = []], Map.put_new(x, :key, :value)) |> strip_ansi() =~
"""
incompatible types given to Map.put_new/3:
Map.put_new(x, :key, :value)
given types:
empty_list(), :key, :value
but expected one of:
map(), term(), term()
"""
end
end
describe "Map.replace/3" do
test "checking" do
assert typecheck!(Map.replace(%{key: 123}, :key, :value)) ==