Skip to content

Commit cc9f4b8

Browse files
authored
Check map updates (#10316)
1 parent b52ddb3 commit cc9f4b8

File tree

5 files changed

+119
-20
lines changed

5 files changed

+119
-20
lines changed

lib/elixir/lib/map.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ defmodule Map do
6868
iex> a
6969
1
7070
71-
But this will raise a match error:
71+
But this will raise a `MatchError` exception:
7272
7373
%{:c => 3} = %{:a => 1, 2 => :b}
7474
@@ -88,8 +88,10 @@ defmodule Map do
8888
iex> map = %{one: 1, two: 2}
8989
iex> %{map | one: "one"}
9090
%{one: "one", two: 2}
91-
iex> %{map | three: 3}
92-
** (KeyError) key :three not found
91+
92+
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
93+
94+
%{map | three: 3}
9395
9496
The functions in this module that need to find a specific key work in logarithmic time.
9597
This means that the time it takes to find keys grows as the map grows, but it's not

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

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,28 +139,20 @@ defmodule Module.Types.Expr do
139139
end
140140

141141
# %{map | ...}
142-
def of_expr({:%{}, _, [{:|, _, [_map, args]}]} = expr, stack, context) do
142+
def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, stack, context) do
143143
stack = push_expr_stack(expr, stack)
144144

145-
case of_pairs(args, stack, context) do
146-
{:ok, _pairs, context} -> {:ok, {:map, [{:optional, :dynamic, :dynamic}]}, context}
147-
{:error, reason} -> {:error, reason}
148-
end
145+
additional_pairs = [{:optional, :dynamic, :dynamic}]
146+
map_update(map, args, additional_pairs, stack, context)
149147
end
150148

151149
# %Struct{map | ...}
152-
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [_map, args]}]}]} = expr, stack, context) do
150+
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, args]}]}]} = expr, stack, context) do
153151
context = Remote.check(module, :__struct__, 0, meta, context)
154152
stack = push_expr_stack(expr, stack)
155153

156-
case of_pairs(args, stack, context) do
157-
{:ok, _pairs, context} ->
158-
pairs = [{:required, {:atom, :__struct__}, {:atom, module}}]
159-
{:ok, {:map, pairs}, context}
160-
161-
{:error, reason} ->
162-
{:error, reason}
163-
end
154+
additional_pairs = [{:required, {:atom, :__struct__}, {:atom, module}}]
155+
map_update(map, args, additional_pairs, stack, context)
164156
end
165157

166158
# %{...}
@@ -395,6 +387,27 @@ defmodule Module.Types.Expr do
395387
end)
396388
end
397389

390+
defp map_update(map, args, additional_pairs, stack, context) do
391+
with {:ok, map_type, context} <- of_expr(map, stack, context),
392+
{:ok, arg_pairs, context} <- of_pairs(args, stack, context),
393+
arg_pairs = pairs_to_unions(arg_pairs, context),
394+
# Change value types to dynamic to reuse map unification for map updates
395+
dynamic_value_pairs =
396+
Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end),
397+
args_type = {:map, additional_pairs ++ dynamic_value_pairs},
398+
{:ok, type, context} <- unify(args_type, map_type, stack, context) do
399+
# Retrieve map type and overwrite with the new value types from the map update
400+
{:map, pairs} = resolve_var(type, context)
401+
402+
updated_pairs =
403+
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
404+
List.keyreplace(pairs, key, 1, {:required, key, value})
405+
end)
406+
407+
{:ok, {:map, updated_pairs}, context}
408+
end
409+
end
410+
398411
defp for_clause({:<-, _, [left, expr]}, stack, context) do
399412
{pattern, guards} = extract_head([left])
400413

lib/elixir/lib/module/types/infer.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ defmodule Module.Types.Infer do
335335
{type, context}
336336
end
337337

338+
def resolve_var({:var, var}, context), do: resolve_var(Map.fetch!(context.types, var), context)
339+
def resolve_var(other, _context), do: other
340+
338341
# Check unify stack to see if variable was already expanded
339342
defp variable_expanded?(var, stack, context) do
340343
Enum.any?(stack.unify_stack, &variable_same?(var, &1, context))

lib/elixir/test/elixir/map_test.exs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ defmodule MapTest do
225225
end
226226
end
227227

228+
defp empty_map(), do: %{}
229+
228230
test "structs" do
229231
assert %ExternalUser{} == %{__struct__: ExternalUser, name: "john", age: 27}
230232

@@ -236,10 +238,8 @@ defmodule MapTest do
236238
%ExternalUser{name: name} = %ExternalUser{}
237239
assert name == "john"
238240

239-
map = %{}
240-
241241
assert_raise BadStructError, "expected a struct named MapTest.ExternalUser, got: %{}", fn ->
242-
%ExternalUser{map | name: "meg"}
242+
%ExternalUser{empty_map() | name: "meg"}
243243
end
244244
end
245245

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,87 @@ defmodule Module.Types.ExprTest do
263263
)
264264
end
265265

266+
test "map update" do
267+
assert quoted_expr(
268+
(
269+
map = %{foo: :a}
270+
%{map | foo: :b}
271+
)
272+
) ==
273+
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :b}}]}}
274+
275+
assert quoted_expr([map], %{map | foo: :b}) ==
276+
{:ok,
277+
{:map, [{:optional, :dynamic, :dynamic}, {:required, {:atom, :foo}, {:atom, :b}}]}}
278+
279+
assert {:error,
280+
{:unable_unify,
281+
{{:map, [{:optional, :dynamic, :dynamic}, {:required, {:atom, :bar}, :dynamic}]},
282+
{:map, [{:required, {:atom, :foo}, {:atom, :a}}]},
283+
_}}} =
284+
quoted_expr(
285+
(
286+
map = %{foo: :a}
287+
%{map | bar: :b}
288+
)
289+
)
290+
end
291+
292+
test "struct update" do
293+
assert quoted_expr(
294+
(
295+
map = %Module.Types.ExprTest.Struct2{field: :a}
296+
%Module.Types.ExprTest.Struct2{map | field: :b}
297+
)
298+
) ==
299+
{:ok,
300+
{:map,
301+
[
302+
{:required, {:atom, :__struct__}, {:atom, Module.Types.ExprTest.Struct2}},
303+
{:required, {:atom, :field}, {:atom, :b}}
304+
]}}
305+
306+
assert {:error,
307+
{:unable_unify,
308+
{{:map,
309+
[
310+
{:required, {:atom, :__struct__}, {:atom, Module.Types.ExprTest.Struct2}},
311+
{:required, {:atom, :field}, :dynamic}
312+
]}, {:map, [{:required, {:atom, :field}, {:atom, :a}}]},
313+
_}}} =
314+
quoted_expr(
315+
(
316+
map = %{field: :a}
317+
%Module.Types.ExprTest.Struct2{map | field: :b}
318+
)
319+
)
320+
321+
assert quoted_expr([map], %Module.Types.ExprTest.Struct2{map | field: :b}) ==
322+
{:ok,
323+
{:map,
324+
[
325+
{:required, {:atom, :__struct__}, {:atom, Module.Types.ExprTest.Struct2}},
326+
{:required, {:atom, :field}, {:atom, :b}}
327+
]}}
328+
329+
assert {:error,
330+
{:unable_unify,
331+
{{:map,
332+
[{:optional, :dynamic, :dynamic}, {:required, {:atom, :not_field}, :dynamic}]},
333+
{:map,
334+
[
335+
{:required, {:atom, :__struct__}, {:atom, Module.Types.ExprTest.Struct2}},
336+
{:required, {:atom, :field}, {:atom, nil}}
337+
]},
338+
_}}} =
339+
quoted_expr(
340+
(
341+
map = %Module.Types.ExprTest.Struct2{}
342+
%{map | not_field: :b}
343+
)
344+
)
345+
end
346+
266347
# Use module attribute to avoid formatter adding parentheses
267348
@mix_module Mix
268349

0 commit comments

Comments
 (0)