Skip to content

Commit 6fabaca

Browse files
committed
Optimize macro traversals
1 parent a08de45 commit 6fabaca

File tree

9 files changed

+114
-78
lines changed

9 files changed

+114
-78
lines changed

lib/elixir/lib/module/types.ex

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ defmodule Module.Types do
66
@no_infer [__protocol__: 1, behaviour_info: 1]
77

88
@doc false
9-
def infer(module, file, defs, private, used, env) do
9+
def infer(module, file, defs, private, defmacrop, env) do
10+
defmacrop = Map.from_keys(defmacrop, [])
1011
finder = &:lists.keyfind(&1, 1, defs)
1112

1213
handler = fn meta, fun_arity, stack, context ->
@@ -29,23 +30,38 @@ defmodule Module.Types do
2930
context = context(%{})
3031

3132
{types, %{local_sigs: local_sigs}} =
32-
for {fun_arity, kind, meta, _clauses} = def <- defs,
33-
kind == :def or kind == :defmacro,
34-
reduce: {[], context} do
33+
for {fun_arity, kind, meta, clauses} = def <- defs, reduce: {[], context} do
3534
{types, context} ->
36-
{_kind, inferred, context} =
37-
local_handler(meta, fun_arity, stack, context, fn _ -> def end)
38-
39-
if kind == :def and fun_arity not in @no_infer do
40-
{[{fun_arity, inferred} | types], context}
41-
else
42-
{types, context}
35+
cond do
36+
kind in [:def, :defmacro] ->
37+
{_kind, inferred, context} =
38+
local_handler(meta, fun_arity, stack, context, fn _ -> def end)
39+
40+
if kind == :def and fun_arity not in @no_infer do
41+
{[{fun_arity, inferred} | types], context}
42+
else
43+
{types, context}
44+
end
45+
46+
kind == :defmacrop and is_map_key(defmacrop, fun_arity) ->
47+
# Bypass the caching structure for defmacrop, that's because
48+
# we don't need them stored in the signatures when we perform
49+
# unreachable checks. This may cause defmacrop to be traversed
50+
# twice if it uses default arguments (which is the only way
51+
# to refer to another defmacrop in definitions).
52+
{_kind, _inferred, context} =
53+
local_handler(fun_arity, kind, meta, clauses, stack, context)
54+
55+
{types, context}
56+
57+
true ->
58+
{types, context}
4359
end
4460
end
4561

4662
unreachable =
4763
for {fun_arity, _kind, _meta, _defaults} = info <- private,
48-
warn_unused_def(info, local_sigs, used, env),
64+
warn_unused_def(info, local_sigs, defmacrop, env),
4965
not is_map_key(local_sigs, fun_arity),
5066
do: fun_arity
5167

@@ -63,7 +79,7 @@ defmodule Module.Types do
6379
end
6480

6581
defp warn_unused_def({fun_arity, kind, meta, 0}, reachable, used, env) do
66-
case meta == false or Map.has_key?(reachable, fun_arity) or fun_arity in used do
82+
case is_map_key(reachable, fun_arity) or is_map_key(used, fun_arity) do
6783
true -> :ok
6884
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
6985
end
@@ -89,7 +105,7 @@ defmodule Module.Types do
89105
defp min_reachable_default(max, min, last, name, reachable, used) when max >= min do
90106
fun_arity = {name, max}
91107

92-
case Map.has_key?(reachable, fun_arity) or fun_arity in used do
108+
case is_map_key(reachable, fun_arity) or is_map_key(used, fun_arity) do
93109
true -> min_reachable_default(max - 1, min, max, name, reachable, used)
94110
false -> min_reachable_default(max - 1, min, last, name, reachable, used)
95111
end
@@ -164,7 +180,7 @@ defmodule Module.Types do
164180

165181
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, stack, context) do
166182
expected = List.duplicate(Descr.dynamic(), arity)
167-
stack = stack |> fresh_stack(fun_arity) |> with_file_meta(meta)
183+
stack = stack |> fresh_stack(kind, fun_arity) |> with_file_meta(meta)
168184

169185
{_, _, mapping, clauses_types, clauses_context} =
170186
Enum.reduce(clauses, {0, 0, [], [], context}, fn
@@ -243,7 +259,7 @@ defmodule Module.Types do
243259

244260
@doc false
245261
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
246-
when mode in [:static, :dynamic, :infer] do
262+
when mode in [:static, :dynamic, :infer, :traversal] do
247263
%{
248264
# The fallback meta used for literals in patterns and guards
249265
meta: [],
@@ -275,6 +291,9 @@ defmodule Module.Types do
275291
#
276292
# * :infer - Same as :dynamic but skips remote calls.
277293
#
294+
# * :traversal - Focused mostly on traversing AST, skips most type system
295+
# operations. Used by macros and functions which already have signatures.
296+
#
278297
# The mode may also control exhaustiveness checks in the future (to be decided).
279298
# We may also want for applications with subtyping in dynamic mode to always
280299
# intersect with dynamic, but this mode may be too lax (to be decided based on
@@ -303,8 +322,15 @@ defmodule Module.Types do
303322
}
304323
end
305324

306-
defp fresh_stack(stack, function) do
307-
%{stack | function: function}
325+
defp fresh_stack(%{mode: mode} = stack, kind, function) do
326+
mode =
327+
cond do
328+
kind in [:defmacro, :defmacrop] and mode == :infer -> :traversal
329+
kind in [:def, :defp] and mode == :traversal -> :infer
330+
true -> mode
331+
end
332+
333+
%{stack | function: function, mode: mode}
308334
end
309335

310336
defp fresh_context(context) do

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ defmodule Module.Types.Apply do
22
# Typing functionality shared between Expr and Pattern.
33
# Generic AST and Enum helpers go to Module.Types.Helpers.
44
@moduledoc false
5-
@max_clauses 32
5+
6+
# We limit the size of the union for two reasons:
7+
# To avoid really large outputs in reports and to
8+
# reduce the computation cost of inferred code.
9+
@max_clauses 16
610

711
alias Module.ParallelChecker
812
import Module.Types.{Helpers, Descr}
@@ -188,7 +192,6 @@ defmodule Module.Types.Apply do
188192
{:erlang, :list_to_float, [{[non_empty_list(integer())], float()}]},
189193
{:erlang, :list_to_integer, [{[non_empty_list(integer())], integer()}]},
190194
{:erlang, :list_to_integer, [{[non_empty_list(integer()), integer()], integer()}]},
191-
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
192195
{:erlang, :make_ref, [{[], reference()}]},
193196
{:erlang, :map_size, [{[open_map()], integer()}]},
194197
{:erlang, :node, [{[], atom()}]},
@@ -217,6 +220,7 @@ defmodule Module.Types.Apply do
217220
{:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]},
218221
{:erlang, :insert_element,
219222
[{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
223+
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
220224
{:erlang, :max, [{[term(), term()], dynamic()}]},
221225
{:erlang, :min, [{[term(), term()], dynamic()}]},
222226
{:erlang, :orelse, [{[boolean(), term()], dynamic()}]},
@@ -256,6 +260,10 @@ defmodule Module.Types.Apply do
256260
257261
Used only by info functions.
258262
"""
263+
def remote(_name, _args_types, _expr, %{mode: :traversal}, context) do
264+
{dynamic(), context}
265+
end
266+
259267
def remote(name, args_types, expr, stack, context) do
260268
arity = length(args_types)
261269

@@ -268,6 +276,10 @@ defmodule Module.Types.Apply do
268276
@doc """
269277
Applies a function in a given module.
270278
"""
279+
def remote(_module, _fun, _args_types, _expr, %{mode: :traversal}, context) do
280+
{dynamic(), context}
281+
end
282+
271283
def remote(:erlang, :element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
272284
when is_integer(index) do
273285
case tuple_fetch(tuple, index - 1) do
@@ -515,11 +527,14 @@ defmodule Module.Types.Apply do
515527
false ->
516528
{dynamic(), context}
517529

530+
{_kind, _info, context} when stack.mode == :traversal ->
531+
{dynamic(), context}
532+
518533
{kind, info, context} ->
519534
case apply_signature(info, args_types, stack) do
520535
{:ok, indexes, type} ->
521536
context =
522-
if stack != :infer and kind == :defp do
537+
if stack.mode != :infer and kind == :defp do
523538
update_in(context.local_used[fun_arity], fn current ->
524539
if info == :none do
525540
[]
@@ -554,10 +569,13 @@ defmodule Module.Types.Apply do
554569

555570
case stack.local_handler.(meta, fun_arity, stack, context) do
556571
false ->
572+
{dynamic(fun()), context}
573+
574+
{_kind, _info, context} when stack.mode == :traversal ->
557575
{fun(), context}
558576

559577
{kind, _info, context} ->
560-
if stack != :infer and kind == :defp do
578+
if stack.mode != :infer and kind == :defp do
561579
# Mark all clauses as used, as the function is being exported.
562580
{fun(), put_in(context.local_used[fun_arity], [])}
563581
else

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ defmodule Module.Types.Expr do
221221
{head_type, context} = of_expr(head, stack, context)
222222

223223
context =
224-
if stack.mode == :infer do
224+
if stack.mode in [:infer, :traversal] do
225225
context
226226
else
227227
case truthness(head_type) do
@@ -386,14 +386,19 @@ defmodule Module.Types.Expr do
386386
)
387387
when is_atom(name) and is_integer(arity) do
388388
{remote_type, context} = of_expr(remote, stack, context)
389+
389390
# TODO: We cannot return the unions of functions. Do we forbid this?
390391
# Do we check it is always the same return type? Do we simply say it is a function?
391392
{mods, context} = Of.modules(remote_type, name, arity, expr, meta, stack, context)
392393

393394
context =
394-
Enum.reduce(mods, context, &(Apply.signature(&1, name, arity, meta, stack, &2) |> elem(1)))
395+
Enum.reduce(
396+
mods,
397+
context,
398+
&(Apply.signature(&1, name, arity, meta, stack, &2) |> elem(1))
399+
)
395400

396-
{fun(), context}
401+
{dynamic(fun()), context}
397402
end
398403

399404
# TODO: &foo/1
@@ -416,8 +421,12 @@ defmodule Module.Types.Expr do
416421
end
417422

418423
# var
419-
def of_expr(var, _stack, context) when is_var(var) do
420-
{Of.var(var, context), context}
424+
def of_expr(var, stack, context) when is_var(var) do
425+
if stack.mode == :traversal do
426+
{dynamic(), context}
427+
else
428+
{Of.var(var, context), context}
429+
end
421430
end
422431

423432
## Try
@@ -551,15 +560,21 @@ defmodule Module.Types.Expr do
551560
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
552561
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
553562

554-
defp of_clauses(clauses, expected, info, stack, {acc, context}) do
563+
defp of_clauses(clauses, expected, info, %{mode: mode} = stack, {acc, context}) do
555564
%{failed: failed?} = context
556565

557566
Enum.reduce(clauses, {acc, context}, fn {:->, meta, [head, body]}, {acc, context} ->
558567
{failed?, context} = reset_failed(context, failed?)
559568
{patterns, guards} = extract_head(head)
560569
{_types, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context)
561570
{body, context} = of_expr(body, stack, context)
562-
{union(acc, body), set_failed(context, failed?)}
571+
context = set_failed(context, failed?)
572+
573+
if mode == :traversal do
574+
{dynamic(), context}
575+
else
576+
{union(acc, body), context}
577+
end
563578
end)
564579
end
565580

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ defmodule Module.Types.Pattern do
2525
is refined, we restart at step 2.
2626
2727
"""
28+
def of_head(_patterns, _guards, expected, _tag, _meta, %{mode: :traversal}, context) do
29+
{expected, context}
30+
end
31+
2832
def of_head(patterns, guards, expected, tag, meta, stack, context) do
2933
stack = %{stack | meta: meta}
3034

@@ -98,7 +102,13 @@ defmodule Module.Types.Pattern do
98102
This version tracks the whole expression in tracing,
99103
instead of only the pattern.
100104
"""
101-
def of_match(pattern, guards \\ [], expected, expr, tag, stack, context) do
105+
def of_match(pattern, guards \\ [], expected, expr, tag, stack, context)
106+
107+
def of_match(_pattern, _guards, expected, _expr, _tag, %{mode: :traversal}, context) do
108+
{expected, context}
109+
end
110+
111+
def of_match(pattern, guards, expected, expr, tag, stack, context) do
102112
context = init_pattern_info(context)
103113
{tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context)
104114

lib/elixir/src/elixir_def.erl

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,10 @@ fun_for(Meta, Module, Name, Arity, Kinds, External) ->
3737
{[{_, Kind, LocalMeta, _, _, _}], ClausesPairs} ->
3838
case (Kinds == all) orelse (lists:member(Kind, Kinds)) of
3939
true ->
40-
TrackLocal = (Kind == defmacrop),
41-
TrackLocal andalso track_local(Module, Tuple),
42-
Local = {value, fun(Fun, Args) -> invoke_local(Meta, Module, Fun, Args, TrackLocal, External) end},
40+
(Kind == defmacrop) andalso track_defmacrop(Module, Tuple),
41+
Local = {value, fun(Fun, Args) -> invoke_local(Meta, Module, Fun, Args, External) end},
4342
Clauses = [Clause || {_, Clause} <- ClausesPairs],
44-
{Kind, elixir_erl:definition_to_anonymous(Kind, LocalMeta, Clauses, Local, External)};
43+
elixir_erl:definition_to_anonymous(Kind, LocalMeta, Clauses, Local, External);
4544
false ->
4645
false
4746
end;
@@ -51,21 +50,20 @@ fun_for(Meta, Module, Name, Arity, Kinds, External) ->
5150
_:_ -> false
5251
end.
5352

54-
invoke_local(Meta, Module, ErlName, Args, TrackLocal, External) ->
53+
invoke_local(Meta, Module, ErlName, Args, External) ->
5554
{Name, Arity} = elixir_utils:erl_fa_to_elixir_fa(ErlName, length(Args)),
5655

5756
case fun_for(Meta, Module, Name, Arity, all, External) of
5857
false ->
5958
{current_stacktrace, [_ | T]} = erlang:process_info(self(), current_stacktrace),
6059
erlang:raise(error, undef, [{Module, Name, Arity, []} | T]);
61-
{Kind, Fun} ->
62-
(TrackLocal and (Kind == defp)) andalso track_local(Module, {Name, Arity}),
60+
Fun ->
6361
apply(Fun, Args)
6462
end.
6563

66-
track_local(Module, FunArity) ->
64+
track_defmacrop(Module, FunArity) ->
6765
{_, Bag} = elixir_module:data_tables(Module),
68-
ets:insert(Bag, {macro_private_calls, FunArity}).
66+
ets:insert(Bag, {defmacrop_calls, FunArity}).
6967

7068
invoke_external(Meta, Mod, Name, Args, E) ->
7169
is_map(E) andalso elixir_env:trace({require, Meta, Mod, []}, E),

lib/elixir/src/elixir_dispatch.erl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,20 +170,20 @@ expand_import(Meta, Name, Arity, E, Extra, AllowLocals, Trace) ->
170170
_ ->
171171
Local = AllowLocals andalso elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E),
172172

173-
case {Dispatch, Local} of
173+
case Dispatch of
174174
%% There is a local and an import. This is a conflict unless
175175
%% the receiver is the same as module (happens on bootstrap).
176-
{{_, Receiver}, {_, _}} when Receiver /= Module ->
176+
{_, Receiver} when Local /= false, Receiver /= Module ->
177177
{conflict, Receiver};
178178

179179
%% There is no local. Dispatch the import.
180-
{_, false} ->
180+
_ when Local == false ->
181181
do_expand_import(Dispatch, Meta, Name, Arity, Module, E, Trace);
182182

183183
%% Dispatch to the local.
184-
{_, {_Kind, Fun}} ->
184+
_ ->
185185
Trace andalso elixir_env:trace({local_macro, Meta, Name, Arity}, E),
186-
{macro, Module, expander_macro_fun(Meta, Fun, Module, Name, E)}
186+
{macro, Module, expander_macro_fun(Meta, Local, Module, Name, E)}
187187
end
188188
end.
189189

lib/elixir/src/elixir_map.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ maybe_load_struct(Meta, Name, Assocs, E) ->
192192
false ->
193193
Name:'__struct__'(Assocs);
194194

195-
{_, ExternalFun} ->
195+
ExternalFun ->
196196
%% There is an inherent race condition when using external_for.
197197
%% By the time we got to execute the function, the ETS table
198198
%% with temporary definitions for the given module may no longer

lib/elixir/src/elixir_module.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
187187
case elixir_config:is_bootstrap() of
188188
true -> {#{}, []};
189189
false ->
190-
Used = bag_lookup_element(DataBag, macro_private_calls, 2),
191-
'Elixir.Module.Types':infer(Module, File, AllDefinitions, NewPrivate, Used, E)
190+
Defmacrop = bag_lookup_element(DataBag, defmacrop_calls, 2),
191+
'Elixir.Module.Types':infer(Module, File, AllDefinitions, NewPrivate, Defmacrop, E)
192192
end,
193193

194194
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),

0 commit comments

Comments
 (0)