Skip to content

Commit 5990faf

Browse files
committed
Infer types and use them across remote calls
1 parent 9d1933b commit 5990faf

File tree

14 files changed

+235
-108
lines changed

14 files changed

+235
-108
lines changed

lib/elixir/lib/code.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ defmodule Code do
249249
:debug_info,
250250
:ignore_already_consolidated,
251251
:ignore_module_conflict,
252+
:infer_signatures,
252253
:relative_paths,
253254
:warnings_as_errors
254255
]
@@ -1638,6 +1639,10 @@ defmodule Code do
16381639
* `:ignore_module_conflict` - when `true`, does not warn when a module has
16391640
already been defined. Defaults to `false`.
16401641
1642+
* `:infer_signatures` (since v1.18.0) - when `false`, it disables inference
1643+
module-local signatures used when type checking remote calls to the compiled
1644+
module. Type checking it still executed. Defaults to `true`.
1645+
16411646
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
16421647
warnings, and errors generated by the compiler. Note disabling this option
16431648
won't affect runtime warnings and errors. Defaults to `true`.

lib/elixir/lib/module/parallel_checker.ex

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ defmodule Module.ParallelChecker do
191191
or if the function does not exist return `{:error, :function}`.
192192
"""
193193
@spec fetch_export(cache(), module(), atom(), arity()) ::
194-
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
194+
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
195+
| {:error, :function | :module}
195196
def fetch_export({server, ets}, module, fun, arity) do
196197
case :ets.lookup(ets, module) do
197198
[] ->
@@ -203,7 +204,7 @@ defmodule Module.ParallelChecker do
203204

204205
[{_key, mode}] ->
205206
case :ets.lookup(ets, {module, {fun, arity}}) do
206-
[{_key, reason}] -> {:ok, mode, reason}
207+
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
207208
[] -> {:error, :function}
208209
end
209210
end
@@ -369,13 +370,13 @@ defmodule Module.ParallelChecker do
369370
true ->
370371
{mode, exports} = info_exports(module)
371372
deprecated = info_deprecated(module)
372-
cache_info(ets, module, exports, deprecated, mode)
373+
cache_info(ets, module, exports, deprecated, %{}, mode)
373374

374375
false ->
375376
# Or load exports from chunk
376377
with {^module, binary, _filename} <- object_code,
377378
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
378-
cache_info(ets, module, exports, %{}, :erlang)
379+
cache_info(ets, module, exports, %{}, %{}, :erlang)
379380
else
380381
_ ->
381382
:ets.insert(ets, {module, false})
@@ -417,25 +418,28 @@ defmodule Module.ParallelChecker do
417418
behaviour_exports(map) ++
418419
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
419420

420-
deprecated = Map.new(map.deprecated)
421-
cache_info(ets, map.module, exports, deprecated, :elixir)
421+
cache_info(ets, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
422422
end
423423

424-
defp cache_info(ets, module, exports, deprecated, mode) do
425-
Enum.each(exports, fn {fun, arity} ->
426-
reason = Map.get(deprecated, {fun, arity})
427-
:ets.insert(ets, {{module, {fun, arity}}, reason})
424+
defp cache_info(ets, module, exports, deprecated, sigs, mode) do
425+
Enum.each(exports, fn fa ->
426+
reason = Map.get(deprecated, fa)
427+
:ets.insert(ets, {{module, fa}, reason, Map.get(sigs, fa, :none)})
428428
end)
429429

430430
:ets.insert(ets, {module, mode})
431431
end
432432

433433
defp cache_chunk(ets, module, exports) do
434434
Enum.each(exports, fn {{fun, arity}, info} ->
435-
:ets.insert(ets, {{module, {fun, arity}}, Map.get(info, :deprecated)})
435+
# TODO: Match on signature directly in Elixir v1.22+
436+
:ets.insert(
437+
ets,
438+
{{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)}
439+
)
436440
end)
437441

438-
:ets.insert(ets, {{module, {:__info__, 1}}, nil})
442+
:ets.insert(ets, {{module, {:__info__, 1}}, nil, :none})
439443
:ets.insert(ets, {module, :elixir})
440444
end
441445

lib/elixir/lib/module/types.ex

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,55 @@
11
defmodule Module.Types do
22
@moduledoc false
33

4-
alias Module.Types.{Expr, Pattern}
4+
alias Module.Types.{Descr, Expr, Pattern}
5+
6+
@doc false
7+
def infer(module, file, defs, env) do
8+
context = context()
9+
10+
for {{fun, arity}, :def, _meta, clauses} <- defs, into: %{} do
11+
stack = stack(:infer, file, module, {fun, arity}, :all, env)
12+
expected = List.duplicate(Descr.dynamic(), arity)
13+
14+
pair_types =
15+
Enum.map(clauses, fn {meta, args, guards, body} ->
16+
try do
17+
{args, context} =
18+
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
19+
20+
{return, _context} = Expr.of_expr(body, stack, context)
21+
{args, return}
22+
rescue
23+
e -> internal_error!(e, __STACKTRACE__, :def, meta, module, fun, args, guards, body)
24+
end
25+
end)
26+
27+
# TODO: Reuse context from patterns and guards
28+
# TODO: Simplify pair types
29+
# TODO: Handle local calls
30+
{{fun, arity}, {:infer, pair_types}}
31+
end
32+
end
533

634
@doc false
735
def warnings(module, file, defs, no_warn_undefined, cache) do
836
context = context()
937

10-
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
11-
stack =
12-
stack(:dynamic, with_file_meta(meta, file), module, function, no_warn_undefined, cache)
38+
Enum.flat_map(defs, fn {{fun, arity}, kind, meta, clauses} ->
39+
file = with_file_meta(meta, file)
40+
stack = stack(:dynamic, file, module, {fun, arity}, no_warn_undefined, cache)
41+
expected = List.duplicate(Descr.dynamic(), arity)
1342

1443
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
1544
try do
16-
warnings_from_clause(meta, args, guards, body, stack, context)
45+
{_types, context} =
46+
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
47+
48+
{_type, context} = Expr.of_expr(body, stack, context)
49+
context.warnings
1750
rescue
1851
e ->
19-
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
20-
21-
error =
22-
RuntimeError.exception("""
23-
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
24-
25-
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
26-
27-
The exception happened while checking this code:
28-
29-
#{Macro.to_string(def_expr)}
30-
31-
Please report this bug at: https://github.com/elixir-lang/elixir/issues
32-
""")
33-
34-
reraise error, __STACKTRACE__
52+
internal_error!(e, __STACKTRACE__, kind, meta, module, fun, args, guards, body)
3553
end
3654
end)
3755
end)
@@ -44,6 +62,25 @@ defmodule Module.Types do
4462
end
4563
end
4664

65+
defp internal_error!(e, stack, kind, meta, module, fun, args, guards, body) do
66+
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
67+
68+
exception =
69+
RuntimeError.exception("""
70+
found error while checking types for #{Exception.format_mfa(module, fun, length(args))}:
71+
72+
#{Exception.format_banner(:error, e, stack)}\
73+
74+
The exception happened while checking this code:
75+
76+
#{Macro.to_string(def_expr)}
77+
78+
Please report this bug at: https://github.com/elixir-lang/elixir/issues
79+
""")
80+
81+
reraise exception, stack
82+
end
83+
4784
defp guards_to_expr([], left) do
4885
left
4986
end
@@ -52,14 +89,6 @@ defmodule Module.Types do
5289
guards_to_expr(guards, {:when, [], [left, guard]})
5390
end
5491

55-
defp warnings_from_clause(meta, args, guards, body, stack, context) do
56-
dynamic = Module.Types.Descr.dynamic()
57-
expected = Enum.map(args, fn _ -> dynamic end)
58-
{_types, context} = Pattern.of_head(args, guards, expected, :default, meta, stack, context)
59-
{_type, context} = Expr.of_expr(body, stack, context)
60-
context.warnings
61-
end
62-
6392
@doc false
6493
def stack(mode, file, module, function, no_warn_undefined, cache)
6594
when mode in [:static, :dynamic, :infer] do
@@ -72,9 +101,9 @@ defmodule Module.Types do
72101
module: module,
73102
# Current function
74103
function: function,
75-
# List of calls to not warn on as undefined
104+
# List of calls to not warn on as undefined or :all
76105
no_warn_undefined: no_warn_undefined,
77-
# A list of cached modules received from the parallel compiler
106+
# A tuple with cache information or a Macro.Env struct indicating no remote traversals
78107
cache: cache,
79108
# The mode controls what happens on function application when
80109
# there are gradual arguments. Non-gradual arguments always

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,19 @@ defmodule Module.Types.Of do
207207
Returns `__info__(:struct)` information about a struct.
208208
"""
209209
def struct_info(struct, meta, stack, context) do
210-
{_, context} = remote(struct, :__struct__, 0, meta, stack, context)
210+
case stack.cache do
211+
%Macro.Env{} = env ->
212+
{Macro.struct_info!(struct, env), context}
211213

212-
info =
213-
struct.__info__(:struct) ||
214-
raise "expected #{inspect(struct)} to return struct metadata, but got none"
214+
_ ->
215+
{_, context} = export(struct, :__struct__, 0, meta, stack, context)
216+
217+
info =
218+
struct.__info__(:struct) ||
219+
raise "expected #{inspect(struct)} to return struct metadata, but got none"
215220

216-
{info, context}
221+
{info, context}
222+
end
217223
end
218224

219225
@doc """
@@ -803,6 +809,21 @@ defmodule Module.Types.Of do
803809
end
804810
end
805811

812+
defp apply_remote({:infer, clauses}, args_types, _stack) do
813+
case for({expected, return} <- clauses, zip_not_disjoint?(args_types, expected), do: return) do
814+
[] ->
815+
domain =
816+
clauses
817+
|> Enum.map(fn {args, _} -> args end)
818+
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
819+
820+
{:error, domain, clauses}
821+
822+
returns ->
823+
{:ok, returns |> Enum.reduce(&union/2) |> dynamic()}
824+
end
825+
end
826+
806827
defp zip_compatible_or_only_gradual?([actual | actuals], [expected | expecteds]) do
807828
(only_gradual?(actual) or compatible?(actual, expected)) and
808829
zip_compatible_or_only_gradual?(actuals, expecteds)
@@ -826,11 +847,15 @@ defmodule Module.Types.Of do
826847
{remote(:module_info, arity), context}
827848
end
828849

850+
defp export(_module, _fun, _arity, _meta, %{cache: %Macro.Env{}}, context) do
851+
{:none, context}
852+
end
853+
829854
defp export(module, fun, arity, meta, stack, context) do
830855
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
831-
{:ok, mode, reason} ->
832-
{remote(fun, arity),
833-
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
856+
{:ok, mode, reason, info} ->
857+
info = if info == :none, do: remote(fun, arity), else: info
858+
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
834859

835860
{:error, type} ->
836861
context =

lib/elixir/src/elixir.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ start(_Type, _Args) ->
8888

8989
%% Compiler options
9090
{docs, true},
91+
{infer_signatures, true},
9192
{ignore_already_consolidated, false},
9293
{ignore_module_conflict, false},
9394
{on_undefined_variable, raise},

lib/elixir/src/elixir_compiler.erl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,12 @@ bootstrap() ->
143143
{ok, _} = application:ensure_all_started(elixir),
144144
elixir_config:static(#{bootstrap => true}),
145145
elixir_config:put(docs, false),
146-
elixir_config:put(relative_paths, false),
147146
elixir_config:put(ignore_module_conflict, true),
147+
elixir_config:put(infer_signatures, false),
148148
elixir_config:put(on_undefined_variable, raise),
149-
elixir_config:put(tracers, []),
150149
elixir_config:put(parser_options, []),
150+
elixir_config:put(relative_paths, false),
151+
elixir_config:put(tracers, []),
151152
{Init, Main} = bootstrap_files(),
152153
{ok, Cwd} = file:get_cwd(),
153154
Lib = filename:join(Cwd, "lib/elixir/lib"),

lib/elixir/src/elixir_erl.erl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -612,14 +612,15 @@ signature_to_binary(_, Name, Signature) ->
612612
Doc = 'Elixir.Inspect.Algebra':format('Elixir.Code':quoted_to_algebra(Quoted), infinity),
613613
'Elixir.IO':iodata_to_binary(Doc).
614614

615-
checker_chunk(Def, #{deprecated := Deprecated, defines_behaviour := DefinesBehaviour}) ->
615+
checker_chunk(Def, #{deprecated := Deprecated, defines_behaviour := DefinesBehaviour, signatures := Signatures}) ->
616616
DeprecatedMap = maps:from_list(Deprecated),
617617

618618
Exports =
619619
[begin
620+
Signature = maps:get(FA, Signatures, none),
620621
Info = case DeprecatedMap of
621-
#{FA := Reason} -> #{deprecated => Reason};
622-
#{} -> #{}
622+
#{FA := Reason} -> #{deprecated => Reason, sig => Signature};
623+
#{} -> #{sig => Signature}
623624
end,
624625
{FA, Info}
625626
end || {FA, _Meta} <- prepend_behaviour_info(DefinesBehaviour, Def)],

lib/elixir/src/elixir_module.erl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
167167
[elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, CallbackE) ||
168168
{VerifyMod, VerifyFun} <- AfterVerify],
169169

170+
Signatures = case elixir_config:get(infer_signatures) of
171+
true -> 'Elixir.Module.Types':infer(Module, File, AllDefinitions, CallbackE);
172+
false -> #{}
173+
end,
174+
170175
ModuleMap = #{
171176
struct => get_struct(DataSet),
172177
module => Module,
@@ -180,7 +185,8 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
180185
compile_opts => CompileOpts,
181186
deprecated => get_deprecated(DataBag),
182187
defines_behaviour => defines_behaviour(DataBag),
183-
impls => Impls
188+
impls => Impls,
189+
signatures => Signatures
184190
},
185191

186192
case ets:member(DataSet, {elixir, taint}) of
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
Code.require_file("type_helper.exs", __DIR__)
2+
3+
defmodule Module.Types.InferTest do
4+
use ExUnit.Case, async: true
5+
6+
import Module.Types.Descr
7+
8+
defmacro infer(config, do: block) do
9+
quote do
10+
defmodule unquote(config).test do
11+
unquote(block)
12+
end
13+
|> runtime_infer()
14+
end
15+
end
16+
17+
defp runtime_infer({:module, module, binary, _result}) do
18+
{:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
19+
{:debug_info_v1, backend, data} = chunk
20+
{:ok, %{signatures: signatures}} = backend.debug_info(:elixir_v1, module, data, [])
21+
signatures
22+
end
23+
24+
test "infer types from patterns", config do
25+
types =
26+
infer config do
27+
def fun1(%y{}, %x{}, x = y, x = Point), do: :ok
28+
def fun2(%x{}, %y{}, x = y, x = Point), do: :ok
29+
def fun3(%y{}, %x{}, x = y, y = Point), do: :ok
30+
def fun4(%x{}, %y{}, x = y, y = Point), do: :ok
31+
end
32+
33+
args = [
34+
dynamic(open_map(__struct__: atom([Point]))),
35+
dynamic(open_map(__struct__: atom([Point]))),
36+
dynamic(atom([Point])),
37+
dynamic(atom([Point]))
38+
]
39+
40+
assert types[{:fun1, 4}] == {:infer, [{args, atom([:ok])}]}
41+
assert types[{:fun2, 4}] == {:infer, [{args, atom([:ok])}]}
42+
assert types[{:fun3, 4}] == {:infer, [{args, atom([:ok])}]}
43+
assert types[{:fun4, 4}] == {:infer, [{args, atom([:ok])}]}
44+
end
45+
end

0 commit comments

Comments
 (0)