Skip to content

Commit ee87c1c

Browse files
committed
Keep precise variables for lists heads and map domain keys, closes #14915
1 parent 6d43358 commit ee87c1c

2 files changed

Lines changed: 67 additions & 27 deletions

File tree

lib/elixir/lib/module/types/pattern.ex

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,11 @@ defmodule Module.Types.Pattern do
336336
end
337337
end
338338

339-
# TODO: Implement domain key types
340-
defp of_pattern_var([{:key, _key} | rest], _type, context) do
341-
of_pattern_var(rest, dynamic(), context)
339+
defp of_pattern_var([{:domain, domain} | rest], type, context) do
340+
case map_get(type, domain) do
341+
{:ok, type} -> of_pattern_var(rest, type, context)
342+
_ -> :error
343+
end
342344
end
343345

344346
defp of_pattern_var([:head | rest], type, context) do
@@ -489,18 +491,27 @@ defmodule Module.Types.Pattern do
489491
end
490492
end)
491493

492-
if dynamic == [] do
493-
{Enum.reduce(static, &intersection/2), context}
494-
else
495-
# The dynamic parts have to be recomputed whenever they change
496-
context = of_var(var, version, [%{root: {:intersection, dynamic}, expr: match}], context)
497-
498-
# And everything else is also pushed as part of the argument intersection
499-
if static == [] do
500-
{{:intersection, dynamic}, context}
501-
else
502-
{{:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}, context}
494+
return =
495+
cond do
496+
dynamic == [] -> Enum.reduce(static, &intersection/2)
497+
static == [] -> {:intersection, dynamic}
498+
true -> {:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}
503499
end
500+
501+
# If the path has domain keys or head, then it is imprecise,
502+
# and we need to keep filtering the match variable itself.
503+
imprecise? =
504+
Enum.any?(path, fn
505+
{:domain, _} -> true
506+
:head -> true
507+
_ -> false
508+
end)
509+
510+
if dynamic == [] and not imprecise? do
511+
{return, context}
512+
else
513+
context = of_var(var, version, [%{root: return, expr: match}], context)
514+
{return, context}
504515
end
505516
end
506517

@@ -643,20 +654,27 @@ defmodule Module.Types.Pattern do
643654
# TODO: Properly handle pin operator in keys
644655
defp of_open_map(args, static, dynamic, path, stack, context) do
645656
{static, dynamic, context} =
646-
Enum.reduce(args, {static, dynamic, context}, fn {key, value}, {static, dynamic, context} ->
647-
{value_type, context} = of_pattern(value, [{:key, key} | path], stack, context)
648-
649-
cond do
650-
# Only atom keys become part of the type because the other keys are divisible
651-
not is_atom(key) ->
652-
{static, dynamic, context}
657+
Enum.reduce(args, {static, dynamic, context}, fn
658+
{key, value}, {static, dynamic, context} when is_atom(key) ->
659+
{value_type, context} = of_pattern(value, [{:key, key} | path], stack, context)
653660

654-
is_descr(value_type) ->
661+
if is_descr(value_type) do
655662
{[{key, value_type} | static], dynamic, context}
656-
657-
true ->
663+
else
658664
{static, [{key, value_type} | dynamic], context}
659-
end
665+
end
666+
667+
{key, value}, {static, dynamic, context} ->
668+
# Keys are always static and won't use the path
669+
{key_type, context} = of_pattern(key, path, stack, context)
670+
true = is_descr(key_type)
671+
672+
{_value_type, context} = of_pattern(value, [{:domain, key_type} | path], stack, context)
673+
674+
# A domain key cannot restrict the map in any way,
675+
# because we are matching only on one possible value
676+
# and we cannot assert anything about any of the others.
677+
{static, dynamic, context}
660678
end)
661679

662680
case dynamic do

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,32 @@ defmodule Module.Types.PatternTest do
204204
end
205205

206206
describe "maps" do
207-
test "fields in patterns" do
207+
test "atom keys in patterns" do
208208
assert typecheck!([x = %{foo: :bar}], x) == dynamic(open_map(foo: atom([:bar])))
209209
assert typecheck!([x = %{123 => 456}], x) == dynamic(open_map())
210210
assert typecheck!([x = %{123 => 456, foo: :bar}], x) == dynamic(open_map(foo: atom([:bar])))
211+
assert typecheck!([%{foo: :bar = x}], x) == dynamic(atom([:bar]))
211212
end
212213

213-
test "fields in guards" do
214+
test "atom keys in guards" do
214215
assert typecheck!([x = %{foo: :bar}], x.bar, x) == dynamic(open_map(foo: atom([:bar])))
215216
end
217+
218+
test "domain keys in patterns" do
219+
assert typecheck!([x = %{123 => 456}], x) == dynamic(open_map())
220+
assert typecheck!([x = %{123 => 456, foo: :bar}], x) == dynamic(open_map(foo: atom([:bar])))
221+
assert typecheck!([%{"123" => :bar = x}], x) == dynamic(atom([:bar]))
222+
end
223+
224+
test "pinned variable key in patterns" do
225+
assert typecheck!(
226+
(
227+
key = 123
228+
%{^key => value} = %{123 => :value}
229+
value
230+
)
231+
) == atom([:value])
232+
end
216233
end
217234

218235
describe "tuples" do
@@ -236,6 +253,11 @@ defmodule Module.Types.PatternTest do
236253
assert typecheck!([x = [:ok | z]], {x, z}) ==
237254
dynamic(tuple([non_empty_list(term(), term()), term()]))
238255

256+
assert typecheck!([x = [a = :a, b = :b, c = :c]], {x, a, b, c}) ==
257+
dynamic(
258+
tuple([non_empty_list(atom([:a, :b, :c])), atom([:a]), atom([:b]), atom([:c])])
259+
)
260+
239261
assert typecheck!([x = [y | z]], {x, y, z}) ==
240262
dynamic(tuple([non_empty_list(term(), term()), term(), term()]))
241263
end

0 commit comments

Comments
 (0)