Skip to content

Commit 1322027

Browse files
committed
Bring infer signatures back
1 parent 6fabaca commit 1322027

File tree

4 files changed

+77
-52
lines changed

4 files changed

+77
-52
lines changed

lib/elixir/lib/code.ex

Lines changed: 12 additions & 2 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+
i:nfer_signatures,
252253
:relative_paths
253254
]
254255

@@ -1561,8 +1562,8 @@ defmodule Code do
15611562
15621563
## Examples
15631564
1564-
Code.compiler_options(ignore_module_conflict: true)
1565-
#=> %{ignore_module_conflict: false}
1565+
Code.compiler_options(infer_signatures: false)
1566+
#=> %{infer_signatures: true}
15661567
15671568
"""
15681569
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1646,6 +1647,15 @@ defmodule Code do
16461647
* `:ignore_module_conflict` - when `true`, does not warn when a module has
16471648
already been defined. Defaults to `false`.
16481649
1650+
* `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local
1651+
signature inference used when type checking remote calls to the compiled
1652+
module. Type checking will be executed regardless of the value of this option.
1653+
Defaults to `true`.
1654+
1655+
`mix test` automatically disables this option via the `:test_elixirc_options`
1656+
project configuration, as there is typically no need to store infer signatures
1657+
for test files.
1658+
16491659
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
16501660
warnings, and errors generated by the compiler. Note disabling this option
16511661
won't affect runtime warnings and errors. Defaults to `true`.

lib/elixir/lib/module/types.ex

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,48 @@ defmodule Module.Types do
22
@moduledoc false
33
alias Module.Types.{Descr, Expr, Pattern, Helpers}
44

5+
# The mode controls what happens on function application when
6+
# there are gradual arguments. Non-gradual arguments always
7+
# perform subtyping and return its output (OUT).
8+
#
9+
# * :strict - Requires types signatures (not implemented).
10+
# * Strong arrows with gradual performs subtyping and returns OUT
11+
# * Weak arrows with gradual performs subtyping and returns OUT
12+
#
13+
# * :static - Type signatures have been given.
14+
# * Strong arrows with gradual performs compatibility and returns OUT
15+
# * Weak arrows with gradual performs compatibility and returns dynamic()
16+
#
17+
# * :dynamic - Type signatures have not been given.
18+
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
19+
# * Weak arrows with gradual performs compatibility and returns dynamic()
20+
#
21+
# * :infer - Same as :dynamic but skips remote calls.
22+
#
23+
# * :traversal - Focused mostly on traversing AST, skips most type system
24+
# operations. Used by macros and when skipping inference.
25+
#
26+
# The mode may also control exhaustiveness checks in the future (to be decided).
27+
# We may also want for applications with subtyping in dynamic mode to always
28+
# intersect with dynamic, but this mode may be too lax (to be decided based on
29+
# feedback).
30+
@modes [:static, :dynamic, :infer, :traversal]
31+
532
# These functions are not inferred because they are added/managed by the compiler
633
@no_infer [__protocol__: 1, behaviour_info: 1]
734

835
@doc false
936
def infer(module, file, defs, private, defmacrop, env) do
37+
infer_signatures? = :elixir_config.get(:infer_signatures)
1038
defmacrop = Map.from_keys(defmacrop, [])
11-
finder = &:lists.keyfind(&1, 1, defs)
39+
40+
finder =
41+
fn fun_arity ->
42+
case :lists.keyfind(fun_arity, 1, defs) do
43+
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
44+
false -> false
45+
end
46+
end
1247

1348
handler = fn meta, fun_arity, stack, context ->
1449
case local_handler(meta, fun_arity, stack, context, finder) do
@@ -34,10 +69,10 @@ defmodule Module.Types do
3469
{types, context} ->
3570
cond do
3671
kind in [:def, :defmacro] ->
37-
{_kind, inferred, context} =
38-
local_handler(meta, fun_arity, stack, context, fn _ -> def end)
72+
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
73+
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
3974

40-
if kind == :def and fun_arity not in @no_infer do
75+
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
4176
{[{fun_arity, inferred} | types], context}
4277
else
4378
{types, context}
@@ -48,9 +83,10 @@ defmodule Module.Types do
4883
# we don't need them stored in the signatures when we perform
4984
# unreachable checks. This may cause defmacrop to be traversed
5085
# twice if it uses default arguments (which is the only way
51-
# to refer to another defmacrop in definitions).
86+
# to refer to another defmacrop in definitions) but that should
87+
# be cheap anyway.
5288
{_kind, _inferred, context} =
53-
local_handler(fun_arity, kind, meta, clauses, stack, context)
89+
local_handler(fun_arity, kind, meta, clauses, :traversal, stack, context)
5490

5591
{types, context}
5692

@@ -68,6 +104,10 @@ defmodule Module.Types do
68104
{Map.new(types), unreachable}
69105
end
70106

107+
defp infer_mode(kind, infer_signatures?) do
108+
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
109+
end
110+
71111
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
72112
env = %{env | function: stack.function, file: stack.file}
73113
tuple = {reason, {fun, arity}, stack.module}
@@ -117,16 +157,21 @@ defmodule Module.Types do
117157

118158
@doc false
119159
def warnings(module, file, defs, no_warn_undefined, cache) do
120-
finder = &:lists.keyfind(&1, 1, defs)
160+
finder = fn fun_arity ->
161+
case :lists.keyfind(fun_arity, 1, defs) do
162+
{_, _, _, _} = clause -> {:dynamic, clause}
163+
false -> false
164+
end
165+
end
166+
121167
handler = &local_handler(&1, &2, &3, &4, finder)
122168
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
123169
context = context(%{})
124170

125171
context =
126172
Enum.reduce(defs, context, fn {fun_arity, _kind, meta, _clauses} = def, context ->
127-
{_kind, _inferred, context} =
128-
local_handler(meta, fun_arity, stack, context, fn _ -> def end)
129-
173+
finder = fn _ -> {:dynamic, def} end
174+
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
130175
context
131176
end)
132177

@@ -161,11 +206,11 @@ defmodule Module.Types do
161206

162207
local_sigs ->
163208
case finder.(fun_arity) do
164-
{fun_arity, kind, meta, clauses} ->
209+
{mode, {fun_arity, kind, meta, clauses}} ->
165210
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
166211

167212
{inferred, mapping, context} =
168-
local_handler(fun_arity, kind, meta, clauses, stack, context)
213+
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
169214

170215
context =
171216
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -178,9 +223,9 @@ defmodule Module.Types do
178223
end
179224
end
180225

181-
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, stack, context) do
226+
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
182227
expected = List.duplicate(Descr.dynamic(), arity)
183-
stack = stack |> fresh_stack(kind, fun_arity) |> with_file_meta(meta)
228+
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
184229

185230
{_, _, mapping, clauses_types, clauses_context} =
186231
Enum.reduce(clauses, {0, 0, [], [], context}, fn
@@ -259,7 +304,7 @@ defmodule Module.Types do
259304

260305
@doc false
261306
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
262-
when mode in [:static, :dynamic, :infer, :traversal] do
307+
when mode in @modes do
263308
%{
264309
# The fallback meta used for literals in patterns and guards
265310
meta: [],
@@ -273,31 +318,7 @@ defmodule Module.Types do
273318
no_warn_undefined: no_warn_undefined,
274319
# A tuple with cache information or a Macro.Env struct indicating no remote traversals
275320
cache: cache,
276-
# The mode controls what happens on function application when
277-
# there are gradual arguments. Non-gradual arguments always
278-
# perform subtyping and return its output (OUT).
279-
#
280-
# * :strict - Requires types signatures (not implemented).
281-
# * Strong arrows with gradual performs subtyping and returns OUT
282-
# * Weak arrows with gradual performs subtyping and returns OUT
283-
#
284-
# * :static - Type signatures have been given.
285-
# * Strong arrows with gradual performs compatibility and returns OUT
286-
# * Weak arrows with gradual performs compatibility and returns dynamic()
287-
#
288-
# * :dynamic - Type signatures have not been given.
289-
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
290-
# * Weak arrows with gradual performs compatibility and returns dynamic()
291-
#
292-
# * :infer - Same as :dynamic but skips remote calls.
293-
#
294-
# * :traversal - Focused mostly on traversing AST, skips most type system
295-
# operations. Used by macros and functions which already have signatures.
296-
#
297-
# The mode may also control exhaustiveness checks in the future (to be decided).
298-
# We may also want for applications with subtyping in dynamic mode to always
299-
# intersect with dynamic, but this mode may be too lax (to be decided based on
300-
# feedback).
321+
# The mode to be used, see the @modes attribute
301322
mode: mode,
302323
# The function for handling local calls
303324
local_handler: handler
@@ -322,15 +343,8 @@ defmodule Module.Types do
322343
}
323344
end
324345

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}
346+
defp fresh_stack(stack, mode, function) when mode in @modes do
347+
%{stack | mode: mode, function: function}
334348
end
335349

336350
defp fresh_context(context) do

lib/elixir/src/elixir.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ start(_Type, _Args) ->
9090
{docs, true},
9191
{ignore_already_consolidated, false},
9292
{ignore_module_conflict, false},
93+
{infer_signatures, true},
9394
{on_undefined_variable, raise},
9495
{parser_options, [{columns, true}]},
9596
{debug_info, true},

lib/mix/lib/mix/compilers/test.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ defmodule Mix.Compilers.Test do
2323
"""
2424
def require_and_run(matched_test_files, test_paths, elixirc_opts, opts) do
2525
elixirc_opts =
26-
Keyword.merge([docs: false, debug_info: false], elixirc_opts)
26+
Keyword.merge([docs: false, debug_info: false, infer_signatures: false], elixirc_opts)
2727

2828
previous_opts = Code.compiler_options(elixirc_opts)
2929

0 commit comments

Comments
 (0)