Skip to content

Commit dfa7333

Browse files
committed
Properly track patterns nested in lists
1 parent aa3d235 commit dfa7333

File tree

4 files changed

+112
-48
lines changed

4 files changed

+112
-48
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,7 +1350,7 @@ defmodule Module.Types.Descr do
13501350
# Optimization: we are removing an open map with one field.
13511351
{:open, fields2, []}, dnf1 when map_size(fields2) == 1 ->
13521352
Enum.reduce(dnf1, [], fn {tag1, fields1, negs1}, acc ->
1353-
{key, value} = Enum.at(fields2, 0)
1353+
{key, value, _rest} = :maps.next(:maps.iterator(fields2))
13541354
t_diff = difference(Map.get(fields1, key, tag_to_type(tag1)), value)
13551355

13561356
if empty?(t_diff) do
@@ -1610,7 +1610,7 @@ defmodule Module.Types.Descr do
16101610
end
16111611

16121612
defp map_fields_to_quoted(tag, map) do
1613-
sorted = Enum.sort(map)
1613+
sorted = Enum.sort(Map.to_list(map))
16141614
keyword? = Inspect.List.keyword?(sorted)
16151615

16161616
for {key, type} <- sorted,

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ defmodule Module.Types.Expr do
3131
@atom_true atom([true])
3232
@exception open_map(__struct__: atom(), __exception__: @atom_true)
3333

34+
args_or_arity = union(list(term()), integer())
35+
36+
extra_info =
37+
list(
38+
tuple([atom([:file]), list(integer())])
39+
|> union(tuple([atom([:line]), integer()]))
40+
|> union(tuple([atom([:error_info]), open_map()]))
41+
)
42+
43+
@stacktrace list(
44+
union(
45+
tuple([atom(), atom(), args_or_arity, extra_info]),
46+
tuple([fun(), args_or_arity, extra_info])
47+
)
48+
)
49+
3450
# :atom
3551
def of_expr(atom, _stack, context) when is_atom(atom),
3652
do: {atom([atom]), context}
@@ -80,10 +96,9 @@ defmodule Module.Types.Expr do
8096
{@caller, context}
8197
end
8298

83-
# TODO: __STACKTRACE__
8499
def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context)
85100
when is_atom(var_context) do
86-
{list(term()), context}
101+
{@stacktrace, context}
87102
end
88103

89104
# {...}

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

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ defmodule Module.Types.Pattern do
4242
end
4343

4444
defp of_pattern_args(patterns, expected_types, stack, context) do
45-
context = %{context | pattern_info: {%{}, %{}}}
45+
context = %{context | pattern_info: {%{}, %{}, 0}}
4646
{trees, context} = of_pattern_args_index(patterns, expected_types, 0, [], stack, context)
4747

4848
{types, context} =
@@ -104,7 +104,7 @@ defmodule Module.Types.Pattern do
104104
the given expected and expr or an error in case of a typing conflict.
105105
"""
106106
def of_match(pattern, expected, expr, stack, context) do
107-
context = %{context | pattern_info: {%{}, %{}}}
107+
context = %{context | pattern_info: {%{}, %{}, 0}}
108108
{tree, context} = of_pattern(pattern, [{:arg, 0, expected, expr}], stack, context)
109109

110110
{[type], context} =
@@ -118,15 +118,15 @@ defmodule Module.Types.Pattern do
118118
end
119119

120120
defp of_pattern_recur(types, stack, context, callback) do
121-
%{pattern_info: {pattern_vars, pattern_args}} = context
121+
%{pattern_info: {pattern_vars, pattern_info, _counter}} = context
122122
context = %{context | pattern_info: nil}
123123
pattern_vars = Map.to_list(pattern_vars)
124124
changed = :lists.seq(0, length(types) - 1)
125125

126126
try do
127127
case callback.(types, changed, context) do
128128
{:ok, types, context} ->
129-
of_pattern_recur(types, pattern_vars, pattern_args, stack, context, callback)
129+
of_pattern_recur(types, pattern_vars, pattern_info, stack, context, callback)
130130

131131
{:error, context} ->
132132
{types, error_vars(pattern_vars, context)}
@@ -136,7 +136,7 @@ defmodule Module.Types.Pattern do
136136
end
137137
end
138138

139-
defp of_pattern_recur(types, vars, args, stack, context, callback) do
139+
defp of_pattern_recur(types, vars, info, stack, context, callback) do
140140
%{vars: context_vars} = context
141141

142142
{changed, context} =
@@ -148,7 +148,7 @@ defmodule Module.Types.Pattern do
148148
[var, {:arg, index, expected, expr} | path], {var_changed?, context} ->
149149
actual = Enum.fetch!(types, index)
150150

151-
case of_pattern_var(path, actual) do
151+
case of_pattern_var(path, actual, info, context) do
152152
{:ok, type} ->
153153
case Of.refine_var(var, type, expr, stack, context) do
154154
{:ok, type, context} ->
@@ -174,9 +174,11 @@ defmodule Module.Types.Pattern do
174174
case paths do
175175
# A single change, check if there are other variables in this index.
176176
[[_var, {:arg, index, _, _} | _]] ->
177-
case args do
178-
%{^index => true} -> {[index | changed], context}
179-
%{^index => false} -> {changed, context}
177+
arg_cons = [:arg | index]
178+
179+
case info do
180+
%{^arg_cons => true} -> {[index | changed], context}
181+
%{^arg_cons => false} -> {changed, context}
180182
end
181183

182184
# Several changes, we have to recompute all indexes.
@@ -195,7 +197,7 @@ defmodule Module.Types.Pattern do
195197
case callback.(types, changed, context) do
196198
# A simple structural comparison for optimization
197199
{:ok, ^types, context} -> {types, context}
198-
{:ok, types, context} -> of_pattern_recur(types, vars, args, stack, context, callback)
200+
{:ok, types, context} -> of_pattern_recur(types, vars, info, stack, context, callback)
199201
{:error, context} -> {types, error_vars(vars, context)}
200202
end
201203
end
@@ -226,40 +228,44 @@ defmodule Module.Types.Pattern do
226228
end
227229
end
228230

229-
defp of_pattern_var([], type) do
231+
defp of_pattern_var([], type, _info, _context) do
230232
{:ok, type}
231233
end
232234

233-
defp of_pattern_var([{:elem, index} | rest], type) when is_integer(index) do
235+
defp of_pattern_var([{:elem, index} | rest], type, info, context) when is_integer(index) do
234236
case tuple_fetch(type, index) do
235-
{_optional?, type} -> of_pattern_var(rest, type)
237+
{_optional?, type} -> of_pattern_var(rest, type, info, context)
236238
_reason -> :error
237239
end
238240
end
239241

240-
defp of_pattern_var([{:key, field} | rest], type) when is_atom(field) do
242+
defp of_pattern_var([{:key, field} | rest], type, info, context) when is_atom(field) do
241243
case map_fetch(type, field) do
242-
{_optional?, type} -> of_pattern_var(rest, type)
244+
{_optional?, type} -> of_pattern_var(rest, type, info, context)
243245
_reason -> :error
244246
end
245247
end
246248

247249
# TODO: Implement domain key types
248-
defp of_pattern_var([{:key, _key} | rest], _type) do
249-
of_pattern_var(rest, dynamic())
250+
defp of_pattern_var([{:key, _key} | rest], _type, info, context) do
251+
of_pattern_var(rest, dynamic(), info, context)
250252
end
251253

252-
defp of_pattern_var([:head | rest], type) do
254+
defp of_pattern_var([{:head, counter} | rest], type, info, context) do
253255
case list_hd(type) do
254-
{_, head} -> of_pattern_var(rest, head)
255-
_ -> :error
256+
{_, head} ->
257+
tree = Map.fetch!(info, [:head | counter])
258+
type = intersection(of_pattern_tree(tree, context), head)
259+
of_pattern_var(rest, type, info, context)
260+
261+
_ ->
262+
:error
256263
end
257264
end
258265

259-
# TODO: This should intersect with the list itself and its tail.
260-
defp of_pattern_var([:tail | rest], type) do
266+
defp of_pattern_var([:tail | rest], type, info, context) do
261267
case list_tl(type) do
262-
{_, tail} -> of_pattern_var(rest, tail)
268+
{_, tail} -> of_pattern_var(rest, tail, info, context)
263269
_ -> :error
264270
end
265271
end
@@ -474,20 +480,21 @@ defmodule Module.Types.Pattern do
474480
when is_atom(name) and is_atom(ctx) do
475481
version = Keyword.fetch!(meta, :version)
476482
[{:arg, arg, _type, _pattern} | _] = path = Enum.reverse(reverse_path)
477-
{vars, args} = context.pattern_info
483+
{vars, info, counter} = context.pattern_info
478484

479485
paths = [[var | path] | Map.get(vars, version, [])]
480486
vars = Map.put(vars, version, paths)
487+
arg_cons = [:arg | arg]
481488

482489
# Our goal here is to compute if an argument has more than one variable.
483-
args =
484-
case args do
485-
%{^arg => false} -> %{args | arg => true}
486-
%{^arg => true} -> args
487-
%{} -> Map.put(args, arg, false)
490+
info =
491+
case info do
492+
%{^arg_cons => false} -> %{info | arg_cons => true}
493+
%{^arg_cons => true} -> info
494+
%{} -> Map.put(info, arg_cons, false)
488495
end
489496

490-
{{:var, version}, %{context | pattern_info: {vars, args}}}
497+
{{:var, version}, %{context | pattern_info: {vars, info, counter}}}
491498
end
492499

493500
# TODO: Properly traverse domain keys
@@ -542,26 +549,45 @@ defmodule Module.Types.Pattern do
542549
# [prefix1, prefix2, prefix3], [prefix1, prefix2 | suffix]
543550
defp of_list(prefix, suffix, path, stack, context) do
544551
{suffix, context} = of_pattern(suffix, [:tail | path], stack, context)
545-
546-
acc =
547-
Enum.reduce(prefix, {[], [], context}, fn arg, {static, dynamic, context} ->
548-
{type, context} = of_pattern(arg, [:head | path], stack, context)
549-
550-
if is_descr(type) do
551-
{[type | static], dynamic, context}
552-
else
553-
{static, [type | dynamic], context}
554-
end
552+
{vars, info, counter} = context.pattern_info
553+
context = %{context | pattern_info: {vars, info, counter + length(prefix)}}
554+
555+
{static, dynamic, info, context} =
556+
Enum.reduce(prefix, {[], [], %{}, context}, fn
557+
arg, {static, dynamic, info, context}
558+
when is_number(arg) or is_atom(arg) or is_binary(arg) or arg == [] ->
559+
{type, context} = of_pattern(arg, [], stack, context)
560+
{[type | static], dynamic, info, context}
561+
562+
arg, {static, dynamic, info, context} ->
563+
counter = map_size(info) + counter
564+
{type, context} = of_pattern(arg, [{:head, counter} | path], stack, context)
565+
info = Map.put(info, [:head | counter], type)
566+
567+
if is_descr(type) do
568+
{[type | static], dynamic, info, context}
569+
else
570+
{static, [type | dynamic], info, context}
571+
end
555572
end)
556573

557-
case acc do
558-
{static, [], context} when is_descr(suffix) ->
574+
context =
575+
if info != %{} do
576+
update_in(context.pattern_info, fn {acc_vars, acc_info, acc_counter} ->
577+
{acc_vars, Map.merge(acc_info, info), acc_counter}
578+
end)
579+
else
580+
context
581+
end
582+
583+
case {static, dynamic} do
584+
{static, []} when is_descr(suffix) ->
559585
{non_empty_list(Enum.reduce(static, &union/2), suffix), context}
560586

561-
{[], dynamic, context} ->
587+
{[], dynamic} ->
562588
{{:non_empty_list, dynamic, suffix}, context}
563589

564-
{static, dynamic, context} ->
590+
{static, dynamic} ->
565591
{{:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix}, context}
566592
end
567593
end

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,29 @@ defmodule Module.Types.ExprTest do
850850
#{hints(:anonymous_rescue)}
851851
"""
852852
end
853+
854+
test "matches on stacktrace" do
855+
# TODO: we are validating the type through the exception but we should actually check the returned type
856+
assert typeerror!(
857+
try do
858+
:ok
859+
rescue
860+
_ ->
861+
[{_, _, args_or_arity, _} | _] = __STACKTRACE__
862+
args_or_arity.fun()
863+
end
864+
) =~ ~l"""
865+
expected a module (an atom) when invoking fun/0 in expression:
866+
867+
args_or_arity.fun()
868+
869+
where "args_or_arity" was given the type:
870+
871+
# type: empty_list() or integer() or non_empty_list(term())
872+
# from: types_test.ex:LINE-3
873+
[{_, _, args_or_arity, _} | _] = __STACKTRACE__
874+
"""
875+
end
853876
end
854877

855878
describe "comprehensions" do

0 commit comments

Comments
 (0)