Skip to content

Commit ff2b738

Browse files
authored
Track domain keys when creating and updating maps (using the map syntax) (#14892)
1 parent e2c4c07 commit ff2b738

File tree

12 files changed

+1108
-810
lines changed

12 files changed

+1108
-810
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ defmodule Module.Types.Apply do
438438
empty?(common) and not (number_type?(left) and number_type?(right)) ->
439439
{:error, :mismatched_comparison}
440440

441-
match?({false, _}, map_fetch(dynamic(common), :__struct__)) ->
441+
match?({false, _}, map_fetch_key(dynamic(common), :__struct__)) ->
442442
{:error, :struct_comparison}
443443

444444
true ->

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

Lines changed: 500 additions & 437 deletions
Large diffs are not rendered by default.

lib/elixir/lib/module/types/expr.ex

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -172,57 +172,38 @@ defmodule Module.Types.Expr do
172172
# allow variables defined on the left side of | to be available
173173
# on the right side, this is safe.
174174
{pairs_types, context} =
175-
Of.pairs(args, expected, stack, context, &of_expr(&1, &2, expr, &3, &4))
175+
Enum.map_reduce(args, context, fn {key, value}, context ->
176+
{key_type, context} = of_expr(key, term(), expr, stack, context)
177+
{value_type, context} = of_expr(value, term(), expr, stack, context)
178+
{{key_type, value_type}, context}
179+
end)
176180

177181
expected =
178182
if stack.mode == :traversal do
179183
expected
180184
else
181-
# TODO: Once we introduce domain keys, if we ever find a domain
182-
# that overlaps atoms, we can only assume optional(atom()) => term(),
183-
# which is what the `open_map()` below falls back into anyway.
184-
Enum.reduce_while(pairs_types, expected, fn
185-
{_, [key], _}, acc ->
186-
case map_fetch_and_put(acc, key, term()) do
187-
{_value, acc} -> {:cont, acc}
188-
_ -> {:halt, open_map()}
185+
# The only information we can attach to the expected types is that
186+
# certain keys are expected.
187+
expected_pairs =
188+
Enum.flat_map(pairs_types, fn {key_type, _value_type} ->
189+
case atom_fetch(key_type) do
190+
{:finite, [key]} -> [{key, term()}]
191+
_ -> []
189192
end
193+
end)
190194

191-
_, _ ->
192-
{:halt, open_map()}
193-
end)
195+
intersection(expected, open_map(expected_pairs))
194196
end
195197

196198
{map_type, context} = of_expr(map, expected, expr, stack, context)
197199

198200
try do
199-
Of.permutate_map(pairs_types, stack, fn fallback, keys_to_assert, pairs ->
200-
# Ensure all keys to assert and all type pairs exist in map
201-
keys_to_assert = Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert
202-
203-
Enum.each(Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert, fn key ->
204-
case map_fetch(map_type, key) do
205-
{_, _} -> :ok
206-
:badkey -> throw({:badkey, map_type, key, update, context})
207-
:badmap -> throw({:badmap, map_type, update, context})
208-
end
209-
end)
210-
211-
# If all keys are known is no fallback (i.e. we know all keys being updated),
212-
# we can update the existing map.
213-
if fallback == none() do
214-
Enum.reduce(pairs, map_type, fn {key, type}, acc ->
215-
case map_fetch_and_put(acc, key, type) do
216-
{_value, descr} -> descr
217-
:badkey -> throw({:badkey, map_type, key, update, context})
218-
:badmap -> throw({:badmap, map_type, update, context})
219-
end
220-
end)
221-
else
222-
# TODO: Use the fallback type to actually indicate if open or closed.
223-
# The fallback must be unioned with the result of map_values with all
224-
# `keys` deleted.
225-
dynamic(open_map(pairs))
201+
Enum.reduce(pairs_types, map_type, fn {key_type, value_type}, acc ->
202+
case map_update(acc, key_type, value_type) do
203+
{:ok, descr} -> descr
204+
{:badkey, key} -> throw({:badkey, map_type, key, update, context})
205+
{:baddomain, domain} -> throw({:baddomain, map_type, domain, update, context})
206+
:badmap -> throw({:badmap, map_type, update, context})
226207
end
227208
end)
228209
catch
@@ -240,13 +221,15 @@ defmodule Module.Types.Expr do
240221
stack,
241222
context
242223
) do
224+
# We pass the expected type as `term()` because the struct update
225+
# operator already expects it to be a map at this point.
243226
{map_type, context} = of_expr(map, term(), struct, stack, context)
244227

245228
context =
246229
if stack.mode == :traversal do
247230
context
248231
else
249-
with {false, struct_key_type} <- map_fetch(map_type, :__struct__),
232+
with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__),
250233
{:finite, [^module]} <- atom_fetch(struct_key_type) do
251234
context
252235
else
@@ -259,8 +242,8 @@ defmodule Module.Types.Expr do
259242
# TODO: Once we support typed structs, we need to type check them here
260243
{type, context} = of_expr(value, term(), expr, stack, context)
261244

262-
case map_fetch_and_put(acc, key, type) do
263-
{_value, acc} -> {acc, context}
245+
case map_put_key(acc, key, type) do
246+
{:ok, acc} -> {acc, context}
264247
_ -> {acc, context}
265248
end
266249
end)
@@ -906,6 +889,27 @@ defmodule Module.Types.Expr do
906889
}
907890
end
908891

892+
def format_diagnostic({:baddomain, type, key_type, expr, context}) do
893+
traces = collect_traces(expr, context)
894+
895+
%{
896+
details: %{typing_traces: traces},
897+
message:
898+
IO.iodata_to_binary([
899+
"""
900+
expected a map with key of type #{to_quoted_string(key_type)} in map update syntax:
901+
902+
#{expr_to_string(expr, collapse_structs: false) |> indent(4)}
903+
904+
but got type:
905+
906+
#{to_quoted_string(type, collapse_structs: false) |> indent(4)}
907+
""",
908+
format_traces(traces)
909+
])
910+
}
911+
end
912+
909913
def format_diagnostic({:badbinary, type, expr, context}) do
910914
traces = collect_traces(expr, context)
911915

0 commit comments

Comments
 (0)