Skip to content

Commit 725ff7e

Browse files
committed
Consider keys may be overridden in a map
1 parent 58a6253 commit 725ff7e

File tree

3 files changed

+144
-27
lines changed

3 files changed

+144
-27
lines changed

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

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,32 @@ defmodule Module.Types.Expr do
118118
def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, stack, context) do
119119
{map_type, context} = of_expr(map, stack, context)
120120

121-
Of.permutate_map(args, stack, context, &of_expr/3, fn _closed?, pairs ->
122-
# TODO: If closed? is false, we need to open up the map
123-
Enum.reduce(pairs, map_type, fn {key, type}, acc ->
124-
case map_put(acc, key, type) do
125-
descr when is_descr(descr) -> descr
126-
:badmap -> throw({:badmap, map_type, expr, context})
127-
end
128-
end)
121+
Of.permutate_map(args, stack, context, &of_expr/3, fn fallback, keys, pairs ->
122+
# If there is no fallback (i.e. it is closed), we can update the existing map,
123+
# otherwise we only assert the existing keys.
124+
keys = if fallback == none(), do: keys, else: Enum.map(pairs, &elem(&1, 0)) ++ keys
125+
126+
# Assert the keys exist
127+
fallback =
128+
Enum.reduce(keys, fallback, fn key, acc ->
129+
case map_fetch(map_type, key) do
130+
{_, value_type} -> union(value_type, acc)
131+
:badkey -> throw({:badkey, map_type, key, expr, context})
132+
:badmap -> throw({:badmap, map_type, expr, context})
133+
end
134+
end)
135+
136+
if fallback == none() do
137+
Enum.reduce(pairs, map_type, fn {key, type}, acc ->
138+
case map_put(acc, key, type) do
139+
descr when is_descr(descr) -> descr
140+
:badmap -> throw({:badmap, map_type, expr, context})
141+
end
142+
end)
143+
else
144+
# TODO: Use the fallback type to actually indicate if open or closed.
145+
open_map(pairs)
146+
end
129147
end)
130148
catch
131149
error -> {error_type(), error(__MODULE__, error, meta, stack, context)}
@@ -579,6 +597,27 @@ defmodule Module.Types.Expr do
579597
}
580598
end
581599

600+
def format_diagnostic({:badkey, type, key, expr, context}) do
601+
traces = collect_traces(expr, context)
602+
603+
%{
604+
details: %{typing_traces: traces},
605+
message:
606+
IO.iodata_to_binary([
607+
"""
608+
expected a map with key #{inspect(key)} in map update syntax:
609+
610+
#{expr_to_string(expr) |> indent(4)}
611+
612+
but got type:
613+
614+
#{to_quoted_string(type) |> indent(4)}
615+
""",
616+
format_traces(traces)
617+
])
618+
}
619+
end
620+
582621
def format_diagnostic({:badbinary, type, expr, context}) do
583622
traces = collect_traces(expr, context)
584623

lib/elixir/lib/module/types/of.ex

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,58 +108,72 @@ defmodule Module.Types.Of do
108108
Builds a closed map.
109109
"""
110110
def closed_map(pairs, stack, context, of_fun) do
111-
permutate_map(pairs, stack, context, of_fun, fn closed?, pairs ->
112-
if closed?, do: closed_map(pairs), else: open_map(pairs)
111+
permutate_map(pairs, stack, context, of_fun, fn fallback, _keys, pairs ->
112+
# TODO: Use the fallback type to actually indicate if open or closed.
113+
if fallback == none(), do: closed_map(pairs), else: open_map(pairs)
113114
end)
114115
end
115116

116117
@doc """
117118
Builds permutation of maps according to the given keys.
118119
"""
119120
def permutate_map(pairs, stack, context, of_fun, of_map) do
120-
{closed?, single, multiple, context} =
121-
Enum.reduce(pairs, {true, [], [], context}, fn
122-
{key, value}, {closed?, single, multiple, context} ->
123-
{keys, context} = of_finite_key_type(key, stack, context, of_fun)
121+
{dynamic?, fallback, single, multiple, assert, context} =
122+
Enum.reduce(pairs, {false, none(), [], [], [], context}, fn
123+
{key, value}, {dynamic?, fallback, single, multiple, assert, context} ->
124+
{dynamic_key?, keys, context} = of_finite_key_type(key, stack, context, of_fun)
124125
{value_type, context} = of_fun.(value, stack, context)
126+
dynamic? = dynamic? or dynamic_key? or gradual?(value_type)
125127

126128
case keys do
127129
:none ->
128-
{false, single, multiple, context}
130+
fallback = union(fallback, value_type)
131+
132+
{fallback, assert} =
133+
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
134+
{union(fallback, type), [key | assert]}
135+
end)
136+
137+
{fallback, assert} =
138+
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
139+
{union(fallback, type), keys ++ assert}
140+
end)
141+
142+
{dynamic?, fallback, [], [], assert, context}
129143

130144
[key] when multiple == [] ->
131-
{closed?, [{key, value_type} | single], multiple, context}
145+
{dynamic?, fallback, [{key, value_type} | single], multiple, assert, context}
132146

133147
keys ->
134-
{closed?, single, [{keys, value_type} | multiple], context}
148+
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context}
135149
end
136150
end)
137151

138152
map =
139153
case Enum.reverse(multiple) do
140154
[] ->
141-
of_map.(closed?, Enum.reverse(single))
155+
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
142156

143157
[{keys, type} | tail] ->
144158
for key <- keys, t <- cartesian_map(tail) do
145-
of_map.(closed?, Enum.reverse(single, [{key, type} | t]))
159+
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
146160
end
147161
|> Enum.reduce(&union/2)
148162
end
149163

150-
{map, context}
164+
if dynamic?, do: {dynamic(map), context}, else: {map, context}
151165
end
152166

153167
defp of_finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
154-
{[key], context}
168+
{false, [key], context}
155169
end
156170

157171
defp of_finite_key_type(key, stack, context, of_fun) do
158172
{key_type, context} = of_fun.(key, stack, context)
159173

160174
case atom_fetch(key_type) do
161-
{:finite, list} -> {list, context}
162-
_ -> {:none, context}
175+
{:finite, list} -> {gradual?(key_type), list, context}
176+
_ -> {gradual?(key_type), :none, context}
163177
end
164178
end
165179

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

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,8 @@ defmodule Module.Types.ExprTest do
481481
end
482482

483483
describe "maps/structs" do
484-
test "creating maps" do
484+
test "creating closed maps" do
485485
assert typecheck!(%{foo: :bar}) == closed_map(foo: atom([:bar]))
486-
assert typecheck!(%{123 => 456}) == open_map()
487-
assert typecheck!(%{123 => 456, foo: :bar}) == open_map(foo: atom([:bar]))
488486
assert typecheck!([x], %{key: x}) == dynamic(closed_map(key: term()))
489487

490488
assert typecheck!(
@@ -495,6 +493,14 @@ defmodule Module.Types.ExprTest do
495493
) == closed_map(foo: atom([:second]))
496494
end
497495

496+
test "creating open maps" do
497+
assert typecheck!(%{123 => 456}) == open_map()
498+
# Since key cannot override :foo, we preserve it
499+
assert typecheck!([key], %{key => 456, foo: :bar}) == dynamic(open_map(foo: atom([:bar])))
500+
# Since key can override :foo, we do not preserve it
501+
assert typecheck!([key], %{:foo => :bar, key => :baz}) == dynamic(open_map())
502+
end
503+
498504
test "creating structs" do
499505
assert typecheck!(%Point{}) ==
500506
closed_map(
@@ -513,7 +519,7 @@ defmodule Module.Types.ExprTest do
513519
)
514520
end
515521

516-
test "updating maps" do
522+
test "updating to closed maps" do
517523
assert typecheck!([x], %{x | x: :zero}) ==
518524
dynamic(open_map(x: atom([:zero])))
519525

@@ -537,6 +543,64 @@ defmodule Module.Types.ExprTest do
537543
"""
538544
end
539545

546+
test "updating to open maps" do
547+
assert typecheck!(
548+
[key],
549+
(
550+
x = %{foo: :bar}
551+
%{x | key => :baz}
552+
)
553+
) == dynamic(open_map())
554+
555+
# Since key cannot override :foo, we preserve it
556+
assert typecheck!(
557+
[key],
558+
(
559+
x = %{foo: :bar}
560+
%{x | key => :baz, foo: :bat}
561+
)
562+
) == dynamic(open_map(foo: atom([:bat])))
563+
564+
# Since key can override :foo, we do not preserve it
565+
assert typecheck!(
566+
[key],
567+
(
568+
x = %{foo: :bar}
569+
%{x | :foo => :baz, key => :bat}
570+
)
571+
) == dynamic(open_map())
572+
573+
# The goal of this test is to verufy we assert keys,
574+
# even if they may be overridden later.
575+
assert typeerror!(
576+
[key],
577+
(
578+
x = %{key: :value}
579+
%{x | :foo => :baz, key => :bat}
580+
)
581+
) == ~l"""
582+
expected a map with key :foo in map update syntax:
583+
584+
%{x | :foo => :baz, key => :bat}
585+
586+
but got type:
587+
588+
%{key: :value}
589+
590+
where "key" was given the type:
591+
592+
# type: dynamic()
593+
# from: types_test.ex:538
594+
key
595+
596+
where "x" was given the type:
597+
598+
# type: %{key: :value}
599+
# from: types_test.ex:540
600+
x = %{key: :value}
601+
"""
602+
end
603+
540604
test "updating structs" do
541605
assert typecheck!([x], %Point{x | x: :zero}) ==
542606
dynamic(open_map(__struct__: atom([Point]), x: atom([:zero])))

0 commit comments

Comments
 (0)