Skip to content

Commit f1bbb2c

Browse files
authored
Infer types from guards and do post-inference on stdlib (#15032)
We now infer types from guards although we don't yet infer conditional types (such as on `or`).
1 parent 3884e7a commit f1bbb2c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1013
-670
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,15 @@ jobs:
102102
path: cover/*
103103

104104
- name: Check reproducible builds
105+
if: ${{ matrix.deterministic }}
106+
run: taskset 1 make check_reproducible
107+
108+
- name: Check git is not required
105109
if: ${{ matrix.deterministic }}
106110
run: |
107111
rm -rf .git
108-
# Recompile System without .git
109-
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
110-
taskset 1 make check_reproducible
112+
cd lib/elixir
113+
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
111114
112115
test_windows:
113116
name: Windows Server 2022, OTP ${{ matrix.otp_version }}

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,10 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
107107
fi
108108
@ echo "==> elixir (compile)";
109109
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
110+
$(Q) $(GENERATE_APP) $(VERSION)
111+
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
110112

111-
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
113+
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP)
112114
$(Q) $(GENERATE_APP) $(VERSION)
113115

114116
unicode: $(UNICODE)

lib/elixir/lib/access.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -873,11 +873,6 @@ defmodule Access do
873873
...> end)
874874
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
875875
876-
An error is raised if the predicate is not a function or is of the incorrect arity:
877-
878-
iex> get_in([], [Access.filter(5)])
879-
** (FunctionClauseError) no function clause matching in Access.filter/1
880-
881876
An error is raised if the accessed structure is not a list:
882877
883878
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
@@ -1154,11 +1149,6 @@ defmodule Access do
11541149
...> end)
11551150
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
11561151
1157-
An error is raised if the predicate is not a function or is of the incorrect arity:
1158-
1159-
iex> get_in([], [Access.find(5)])
1160-
** (FunctionClauseError) no function clause matching in Access.find/1
1161-
11621152
An error is raised if the accessed structure is not a list:
11631153
11641154
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])

lib/elixir/lib/code.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,10 +1741,10 @@ defmodule Code do
17411741
module. Type checking will be executed regardless of the value of this option.
17421742
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
17431743
1744-
When setting this option, we recommend running `mix clean` so the current module
1745-
may be compiled from scratch. `mix test` automatically disables this option via
1746-
the `:test_elixirc_options` project configuration, as there is typically no need
1747-
to infer signatures for test files.
1744+
When setting this option, we recommend running `mix clean` so the modules can be
1745+
recompiled with the new behaviour. `mix test` automatically disables this option
1746+
via the `:test_elixirc_options` project configuration, as there is typically no
1747+
need to infer signatures for test files.
17481748
17491749
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
17501750
warnings, and errors generated by the compiler. Note disabling this option

lib/elixir/lib/exception.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,8 +1930,8 @@ defmodule FunctionClauseError do
19301930
19311931
For example:
19321932
1933-
iex> URI.parse(:wrong_argument)
1934-
** (FunctionClauseError) no function clause matching in URI.parse/1
1933+
iex> List.duplicate(:ok, -3)
1934+
** (FunctionClauseError) no function clause matching in List.duplicate/2
19351935
19361936
The following fields of this exception are public and can be accessed freely:
19371937

lib/elixir/lib/kernel.ex

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4764,8 +4764,8 @@ defmodule Kernel do
47644764
defp in_range(left, first, last, step) do
47654765
quoted =
47664766
quote do
4767-
:erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
4768-
:erlang.is_integer(unquote(last)) and
4767+
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and
4768+
unquote(generated_is_integer(last)) and
47694769
((:erlang.>(unquote(step), 0) and
47704770
unquote(increasing_compare(left, first, last))) or
47714771
(:erlang.<(unquote(step), 0) and
@@ -4782,8 +4782,8 @@ defmodule Kernel do
47824782
defp in_range_literal(left, first, last, step) when step > 0 do
47834783
quoted =
47844784
quote do
4785-
:erlang.andalso(
4786-
:erlang.is_integer(unquote(left)),
4785+
Kernel.and(
4786+
unquote(generated_is_integer(left)),
47874787
unquote(increasing_compare(left, first, last))
47884788
)
47894789
end
@@ -4794,8 +4794,8 @@ defmodule Kernel do
47944794
defp in_range_literal(left, first, last, step) when step < 0 do
47954795
quoted =
47964796
quote do
4797-
:erlang.andalso(
4798-
:erlang.is_integer(unquote(left)),
4797+
Kernel.and(
4798+
unquote(generated_is_integer(left)),
47994799
unquote(decreasing_compare(left, first, last))
48004800
)
48014801
end
@@ -4809,7 +4809,7 @@ defmodule Kernel do
48094809

48104810
defp in_range_step(quoted, left, first, step) do
48114811
quote do
4812-
:erlang.andalso(
4812+
Kernel.and(
48134813
unquote(quoted),
48144814
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
48154815
)
@@ -4818,7 +4818,7 @@ defmodule Kernel do
48184818

48194819
defp in_list(left, head, tail, expand, right, in_body?) do
48204820
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
4821-
:lists.foldl(&quote(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
4821+
:lists.foldl(&quote(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
48224822
end
48234823

48244824
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
@@ -4828,15 +4828,15 @@ defmodule Kernel do
48284828

48294829
[tail_head | tail] ->
48304830
quote do
4831-
:erlang.orelse(
4831+
Kernel.or(
48324832
:erlang."=:="(unquote(left), unquote(head)),
48334833
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
48344834
)
48354835
end
48364836

48374837
tail when in_body? ->
48384838
quote do
4839-
:erlang.orelse(
4839+
Kernel.or(
48404840
:erlang."=:="(unquote(left), unquote(head)),
48414841
:lists.member(unquote(left), unquote(tail))
48424842
)
@@ -4851,9 +4851,13 @@ defmodule Kernel do
48514851
quote(do: :erlang."=:="(unquote(left), unquote(right)))
48524852
end
48534853

4854+
defp generated_is_integer(arg) do
4855+
quote generated: true, do: :erlang.is_integer(unquote(arg))
4856+
end
4857+
48544858
defp increasing_compare(var, first, last) do
48554859
quote do
4856-
:erlang.andalso(
4860+
Kernel.and(
48574861
:erlang.>=(unquote(var), unquote(first)),
48584862
:erlang."=<"(unquote(var), unquote(last))
48594863
)
@@ -4862,7 +4866,7 @@ defmodule Kernel do
48624866

48634867
defp decreasing_compare(var, first, last) do
48644868
quote do
4865-
:erlang.andalso(
4869+
Kernel.and(
48664870
:erlang."=<"(unquote(var), unquote(first)),
48674871
:erlang.>=(unquote(var), unquote(last))
48684872
)

lib/elixir/lib/list.ex

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,10 @@ defmodule List do
187187
"""
188188
@spec duplicate(any, 0) :: []
189189
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
190-
def duplicate(elem, n) do
191-
:lists.duplicate(n, elem)
192-
end
190+
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, [])
191+
192+
defp duplicate(0, _elem, acc), do: acc
193+
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
193194

194195
@doc """
195196
Flattens the given `list` of nested lists.

lib/elixir/lib/module/parallel_checker.ex

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,6 @@ defmodule Module.ParallelChecker do
220220
end
221221
end
222222

223-
@doc """
224-
Test cache.
225-
"""
226-
def test_cache do
227-
{:ok, cache} = start_link()
228-
cache
229-
end
230-
231223
@doc """
232224
Returns the export kind and deprecation reason for the given MFA from
233225
the cache. If the module does not exist return `:badmodule`,

lib/elixir/lib/module/types.ex

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,17 @@ defmodule Module.Types do
2424
#
2525
# * :infer - Same as :dynamic but skips remote calls.
2626
#
27-
# * :traversal - Focused mostly on traversing AST, skips most type system
28-
# operations. Used by macros and when skipping inference.
29-
#
3027
# The mode may also control exhaustiveness checks in the future (to be decided).
3128
# We may also want for applications with subtyping in dynamic mode to always
3229
# intersect with dynamic, but this mode may be too lax (to be decided based on
3330
# feedback).
34-
@modes [:static, :dynamic, :infer, :traversal]
31+
@modes [:static, :dynamic, :infer]
3532

3633
# These functions are not inferred because they are added/managed by the compiler
3734
@no_infer [behaviour_info: 1]
3835

3936
@doc false
40-
def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
37+
def infer(module, file, attrs, defs, used_private, env, {_, cache}) do
4138
# We don't care about inferring signatures for protocols,
4239
# those will be replaced anyway. There is also nothing to
4340
# infer if there is no cache system, we only do traversals.
@@ -75,11 +72,12 @@ defmodule Module.Types do
7572

7673
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
7774

78-
{types, %{local_sigs: reachable_sigs} = context} =
79-
for {fun_arity, kind, meta, _clauses} = def <- defs,
80-
kind in [:def, :defmacro],
81-
reduce: {[], context()} do
82-
{types, context} ->
75+
# In case there are loops, the other we traverse matters,
76+
# so we sort the definitions for determinism
77+
{types, private, %{local_sigs: reachable_sigs} = context} =
78+
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs),
79+
reduce: {[], [], context()} do
80+
{types, private, context} when kind in [:def, :defmacro] ->
8381
# Optimized version of finder, since we already have the definition
8482
finder = fn _ ->
8583
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
@@ -88,10 +86,13 @@ defmodule Module.Types do
8886
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
8987

9088
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
91-
{[{fun_arity, inferred} | types], context}
89+
{[{fun_arity, inferred} | types], private, context}
9290
else
93-
{types, context}
91+
{types, private, context}
9492
end
93+
94+
{types, private, context} ->
95+
{types, [def | private], context}
9596
end
9697

9798
# Now traverse all used privates to find any other private that have been used by them.
@@ -105,8 +106,8 @@ defmodule Module.Types do
105106

106107
{unreachable, _context} =
107108
Enum.reduce(private, {[], context}, fn
108-
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
109-
warn_unused_def(info, used_sigs, env)
109+
{fun_arity, kind, meta, _clauses}, {unreachable, context} ->
110+
warn_unused_def(fun_arity, kind, meta, used_sigs, env)
110111

111112
# Find anything undefined within unused functions
112113
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
@@ -125,7 +126,7 @@ defmodule Module.Types do
125126
end
126127

127128
defp infer_mode(kind, infer_signatures?) do
128-
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
129+
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traverse
129130
end
130131

131132
defp protocol?(attrs) do
@@ -154,7 +155,7 @@ defmodule Module.Types do
154155
| List.duplicate(Descr.dynamic(), arity - 1)
155156
]
156157

157-
{fun_arity, kind, meta, clauses} = def
158+
{_fun_arity, kind, meta, clauses} = def
158159

159160
clauses =
160161
for {meta, args, guards, body} <- clauses do
@@ -173,29 +174,30 @@ defmodule Module.Types do
173174
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
174175
end
175176

176-
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
177-
:ok
178-
end
177+
defp warn_unused_def(fun_arity, kind, meta, used, env) do
178+
default = Keyword.get(meta, :defaults, 0)
179179

180-
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
181-
case is_map_key(used, fun_arity) do
182-
true -> :ok
183-
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
184-
end
180+
cond do
181+
Keyword.get(meta, :context) != nil ->
182+
:ok
185183

186-
:ok
187-
end
184+
default == 0 ->
185+
case is_map_key(used, fun_arity) do
186+
true -> :ok
187+
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
188+
end
188189

189-
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
190-
{name, arity} = tuple
191-
min = arity - default
192-
max = arity
190+
default > 0 ->
191+
{name, arity} = fun_arity
192+
min = arity - default
193+
max = arity
193194

194-
case min_reachable_default(max, min, :none, name, used) do
195-
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
196-
^min -> :ok
197-
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
198-
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
195+
case min_reachable_default(max, min, :none, name, used) do
196+
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
197+
^min -> :ok
198+
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity})
199+
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff})
200+
end
199201
end
200202

201203
:ok
@@ -291,7 +293,7 @@ defmodule Module.Types do
291293
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
292294

293295
{inferred, mapping, context} =
294-
local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
296+
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
295297

296298
context =
297299
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -304,7 +306,17 @@ defmodule Module.Types do
304306
end
305307
end
306308

307-
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
309+
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
310+
context =
311+
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
312+
Module.Types.Traverse.of_expr(body, stack, context)
313+
end)
314+
315+
inferred = {:infer, nil, [{List.duplicate(Descr.term(), arity), Descr.dynamic()}]}
316+
{inferred, [{0, 0}], context}
317+
end
318+
319+
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
308320
{fun, _arity} = fun_arity
309321
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
310322

@@ -320,12 +332,7 @@ defmodule Module.Types do
320332
{return_type, context} =
321333
Expr.of_expr(body, Descr.term(), body, stack, context)
322334

323-
args_types =
324-
if stack.mode == :traversal do
325-
expected
326-
else
327-
Pattern.of_domain(trees, context)
328-
end
335+
args_types = Pattern.of_domain(trees, context)
329336

330337
{type_index, inferred} =
331338
add_inferred(inferred, args_types, return_type, total - 1, [])
@@ -442,7 +449,9 @@ defmodule Module.Types do
442449
warnings: [],
443450
# All vars and their types
444451
vars: %{},
445-
# Variables and arguments from patterns
452+
# Variables that are specific to the current environment/conditional
453+
conditional_vars: nil,
454+
# Track metadata specific to matches and guards
446455
pattern_info: nil,
447456
# If type checking has found an error/failure
448457
failed: false,

0 commit comments

Comments
 (0)