Perform return type inference and application across local calls (#13984)

This commit is contained in:
José Valim
2024-11-11 23:06:31 +01:00
committed by GitHub
parent 6689eb9462
commit cb2e03688e
39 changed files with 1765 additions and 1747 deletions
+6 -4
View File
@@ -6,13 +6,15 @@ TODO.
* Type inference of patterns (typing inference of guards will be part of an upcoming release)
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
* Type checking of all language constructs, except `for`, `with`, and closures
* Type checking of all language constructs, including local and remote calls, except `for`, `with`, and closures
* Type checking of all `Kernel` and conversion functions inlined by the compiler
* Detection of clauses and patterns that never match
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
* Detection of clauses and patterns that will never match from `case`, `cond`, and `=`
* Detection of unused clauses from private functions
## ExUnit improvements
+2 -9
View File
@@ -797,15 +797,8 @@ defmodule Time do
@doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do
case convert(time, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
"reason: #{inspect(reason)}"
end
{:ok, value} = convert(time, calendar)
value
end
@doc """
+1 -1
View File
@@ -1649,7 +1649,7 @@ defmodule Code do
* `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local
signature inference used when type checking remote calls to the compiled
module. Type checking will be executed regardless of this value of this option.
module. Type checking will be executed regardless of the value of this option.
Defaults to `true`.
`mix test` automatically disables this option via the `:test_elixirc_options`
+3 -3
View File
@@ -302,7 +302,7 @@ defmodule Kernel do
@compile {:inline, bootstrapped?: 1}
case :code.ensure_loaded(Kernel) do
{:module, _} ->
defp bootstrapped?(_), do: true
defp bootstrapped?(module), do: is_atom(module)
{:error, _} ->
defp bootstrapped?(module), do: :code.ensure_loaded(module) == {:module, module}
@@ -3646,7 +3646,7 @@ defmodule Kernel do
:ok
true ->
pos = :elixir_locals.cache_env(__CALLER__)
pos = :elixir_module.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
@@ -5323,7 +5323,7 @@ defmodule Kernel do
key
end
pos = :elixir_locals.cache_env(env)
pos = :elixir_module.cache_env(env)
quote do
:elixir_def.store_definition(unquote(kind), unquote(store), unquote(pos))
+1 -1
View File
@@ -196,7 +196,7 @@ defmodule Kernel.CLI do
end
@elixir_internals [:elixir, :elixir_aliases, :elixir_expand, :elixir_compiler, :elixir_module] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map, :elixir_locals] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker]
+5 -5
View File
@@ -138,7 +138,7 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do
{name, arity} = signature when signature in @reserved_signatures ->
compile_error(
:elixir_locals.get_cached_env(pos),
:elixir_module.get_cached_env(pos),
"type #{name}/#{arity} is a reserved type and it cannot be defined"
)
@@ -247,7 +247,7 @@ defmodule Kernel.Typespec do
defp collect_defined_type_pairs(type_typespecs) do
fun = fn {_kind, expr, pos}, type_pairs ->
%{file: file, line: line} = env = :elixir_locals.get_cached_env(pos)
%{file: file, line: line} = env = :elixir_module.get_cached_env(pos)
case type_to_signature(expr) do
{name, arity} = type_pair ->
@@ -292,7 +292,7 @@ defmodule Kernel.Typespec do
defp translate_type({kind, {:"::", _, [{name, meta, args}, definition]}, pos}, state)
when is_list(meta) do
caller = :elixir_locals.get_cached_env(pos)
caller = :elixir_module.get_cached_env(pos)
state = clean_local_state(state)
args =
@@ -349,12 +349,12 @@ defmodule Kernel.Typespec do
defp underspecified?(_kind, _arity, _spec), do: false
defp translate_spec({kind, {:when, _meta, [spec, guard]}, pos}, state) do
caller = :elixir_locals.get_cached_env(pos)
caller = :elixir_module.get_cached_env(pos)
translate_spec(kind, spec, guard, caller, state)
end
defp translate_spec({kind, spec, pos}, state) do
caller = :elixir_locals.get_cached_env(pos)
caller = :elixir_module.get_cached_env(pos)
translate_spec(kind, spec, [], caller, state)
end
+2 -11
View File
@@ -1415,15 +1415,7 @@ defmodule Module do
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
assert_not_readonly!(__ENV__.function, module)
case :elixir_def.take_definition(module, {name, arity}) do
false ->
false
_ ->
:elixir_locals.yank({name, arity}, module)
true
end
:elixir_def.take_definition(module, {name, arity}) != false
end
@doc """
@@ -1452,8 +1444,7 @@ defmodule Module do
"overridable because it was not defined"
clause ->
neighbours = :elixir_locals.yank(tuple, module)
:elixir_overridable.record_overridable(module, tuple, clause, neighbours)
:elixir_overridable.record_overridable(module, tuple, clause)
end
other ->
-258
View File
@@ -1,258 +0,0 @@
# This is an Elixir module responsible for tracking
# calls in order to extract Elixir modules' behaviour
# during compilation time.
#
# ## Implementation
#
# The implementation uses ETS to track all dependencies
# resembling a graph. The keys and what they point to are:
#
# * `:reattach` points to `{name, arity}`
# * `{:local, {name, arity}}` points to `{{name, arity}, line, macro_dispatch?}`
# * `{:import, {name, arity}}` points to `Module`
#
# This is built on top of the internal module tables.
defmodule Module.LocalsTracker do
@moduledoc false
@defmacros [:defmacro, :defmacrop]
@doc """
Adds and tracks defaults for a definition into the tracker.
"""
def add_defaults({_set, bag}, kind, {name, arity} = pair, defaults, meta) do
for i <- :lists.seq(arity - defaults, arity - 1) do
put_edge(bag, {:local, {name, i}}, {pair, get_line(meta), kind in @defmacros})
end
:ok
end
@doc """
Adds a local dispatch from-to the given target.
"""
def add_local({_set, bag}, from, to, meta, macro_dispatch?)
when is_tuple(from) and is_tuple(to) and is_boolean(macro_dispatch?) do
put_edge(bag, {:local, from}, {to, get_position(meta), macro_dispatch?})
:ok
end
@doc """
Adds an import dispatch to the given target.
"""
def add_import({set, _bag}, function, module, imported)
when is_tuple(function) and is_atom(module) do
put_edge(set, {:import, imported}, module)
:ok
end
@doc """
Yanks a local node. Returns its in and out vertices in a tuple.
"""
def yank({_set, bag}, local) do
:lists.usort(take_out_neighbours(bag, {:local, local}))
end
@doc """
Reattach a previously yanked node.
"""
def reattach({_set, bag}, tuple, kind, function, out_neighbours, meta) do
for out_neighbour <- out_neighbours do
put_edge(bag, {:local, function}, out_neighbour)
end
# Make a call from the old function to the new one
if function != tuple do
put_edge(bag, {:local, function}, {tuple, get_line(meta), kind in @defmacros})
end
# Finally marked the new one as reattached
put_edge(bag, :reattach, tuple)
:ok
end
@doc """
Collect all conflicting imports with the given functions
"""
def collect_imports_conflicts({set, _bag}, all_defined) do
for {pair, _, meta, _} <- all_defined, n = out_neighbour(set, {:import, pair}) do
{meta, {n, pair}}
end
end
@doc """
Collect all unused definitions based on the private
given, also accounting the expected number of default
clauses a private function have.
"""
def collect_unused_locals({_set, bag}, all_defined, private) do
reachable =
Enum.reduce(all_defined, %{}, fn {pair, kind, _, _}, acc ->
if kind in [:def, :defmacro] do
reachable_from(bag, pair, acc)
else
acc
end
end)
reattached = :lists.usort(out_neighbours(bag, :reattach))
{unreachable(reachable, reattached, private), collect_warnings(reachable, private)}
end
@doc """
Collect undefined functions based on local calls and existing definitions.
"""
def collect_undefined_locals({set, bag}, all_defined, file) do
undefined =
for {pair, _, meta, _} <- all_defined,
{{local_name, _} = local, position, macro_dispatch?} <-
out_neighbours(bag, {:local, pair}),
error = undefined_local_error(set, local, macro_dispatch?) do
file =
case Keyword.get(meta, :file) do
{keep_file, _keep_line} -> keep_file
nil -> file
end
meta = build_meta(position, local_name)
{pair, meta, file, local, error}
end
:lists.usort(undefined)
end
defp undefined_local_error(set, local, true) do
case :ets.member(set, {:def, local}) do
true -> false
false -> :undefined_function
end
end
defp undefined_local_error(set, local, false) do
try do
if :ets.lookup_element(set, {:def, local}, 2) in @defmacros do
:incorrect_dispatch
else
false
end
catch
_, _ -> :undefined_function
end
end
defp unreachable(reachable, reattached, private) do
for {tuple, kind, _, _} <- private,
not reachable?(tuple, kind, reachable, reattached),
do: tuple
end
defp reachable?(tuple, :defmacrop, reachable, reattached) do
# All private macros are unreachable unless they have been
# reattached and they are reachable.
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
end
defp reachable?(tuple, :defp, reachable, _reattached) do
Map.has_key?(reachable, tuple)
end
defp collect_warnings(reachable, private) do
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
end
defp collect_warnings({_, _, false, _}, acc, _reachable) do
acc
end
defp collect_warnings({tuple, kind, meta, 0}, acc, reachable) do
if Map.has_key?(reachable, tuple) do
acc
else
[{meta, {:unused_def, tuple, kind}} | acc]
end
end
defp collect_warnings({tuple, kind, meta, default}, acc, reachable) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, reachable) do
:none -> [{meta, {:unused_def, tuple, kind}} | acc]
^min -> acc
^max -> [{meta, {:unused_args, tuple}} | acc]
diff -> [{meta, {:unused_args, tuple, diff}} | acc]
end
end
defp min_reachable_default(max, min, last, name, reachable) when max >= min do
case Map.has_key?(reachable, {name, max}) do
true -> min_reachable_default(max - 1, min, max, name, reachable)
false -> min_reachable_default(max - 1, min, last, name, reachable)
end
end
defp min_reachable_default(_max, _min, last, _name, _reachable) do
last
end
@doc """
Returns all local nodes reachable from `vertex`.
By default, all public functions are reachable.
A private function is only reachable if it has
a public function that it invokes directly.
"""
def reachable_from({_, bag}, local) do
bag
|> reachable_from(local, %{})
|> Map.keys()
end
defp reachable_from(bag, local, vertices) do
vertices = Map.put(vertices, local, true)
Enum.reduce(out_neighbours(bag, {:local, local}), vertices, fn {local, _line, _}, acc ->
case acc do
%{^local => true} -> acc
_ -> reachable_from(bag, local, acc)
end
end)
end
defp get_line(meta), do: Keyword.get(meta, :line)
defp get_position(meta), do: {get_line(meta), meta[:column]}
defp build_meta(position, function_name) do
case position do
{line, nil} -> [line: line]
{line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name)
end
end
## Lightweight digraph implementation
defp put_edge(d, from, to) do
:ets.insert(d, {from, to})
end
defp out_neighbour(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> nil
end
end
defp out_neighbours(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> []
end
end
defp take_out_neighbours(d, from) do
Keyword.values(:ets.take(d, from))
end
end
+58 -52
View File
@@ -5,7 +5,6 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type kind() :: :def | :defmacro
@type mode() :: :elixir | :erlang
@doc """
@@ -48,7 +47,12 @@ defmodule Module.ParallelChecker do
@doc """
Spawns a process that runs the parallel checker.
"""
def spawn({pid, checker}, module, info, log?) do
def spawn({pid, checker}, module, module_map, log?) do
ets = :gen_server.call(checker, :ets, :infinity)
inner_spawn(pid, checker, module, cache_from_module_map(ets, module_map), log?)
end
defp inner_spawn(pid, checker, module, info, log?) do
ref = make_ref()
spawned =
@@ -59,17 +63,22 @@ defmodule Module.ParallelChecker do
{^ref, :cache, ets} ->
Process.link(pid)
module_map =
if is_map(info) do
info
else
case File.read(info) do
{:ok, binary} -> maybe_module_map(binary, module)
{:error, _} -> nil
end
module_tuple =
cond do
is_tuple(info) ->
info
is_binary(info) ->
with {:ok, binary} <- File.read(info),
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
cache_from_module_map(ets, module_map)
else
_ -> nil
end
end
module_map && cache_from_module_map(ets, module_map)
send(checker, {ref, :cached})
receive do
@@ -78,8 +87,8 @@ defmodule Module.ParallelChecker do
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
if module_map do
check_module(module_map, {checker, ets}, log?)
if module_tuple do
check_module(module_tuple, {checker, ets}, log?)
else
[]
end
@@ -147,7 +156,7 @@ defmodule Module.ParallelChecker do
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
spawn({self(), checker}, module, file, log?)
inner_spawn(self(), checker, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
@@ -212,29 +221,9 @@ defmodule Module.ParallelChecker do
## Module checking
defp check_module(module_map, cache, log?) do
%{
module: module,
file: file,
compile_opts: compile_opts,
definitions: definitions,
attributes: attributes,
impls: impls
} = module_map
# TODO: Match on anno directly in Elixir v1.22+
line =
case module_map do
%{anno: anno} -> :erl_anno.line(anno)
%{line: line} -> line
end
behaviours = for {:behaviour, module} <- attributes, do: module
no_warn_undefined =
compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
defp check_module(module_tuple, cache, log?) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} =
module_tuple
behaviour_warnings =
Module.Behaviour.check_behaviours_and_impls(
@@ -253,13 +242,41 @@ defmodule Module.ParallelChecker do
|> group_warnings()
|> emit_warnings(log?)
module_map
|> Map.get(:after_verify, [])
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
diagnostics
end
defp module_map_to_module_tuple(module_map) do
%{
module: module,
file: file,
compile_opts: compile_opts,
definitions: definitions,
attributes: attributes,
impls: impls,
after_verify: after_verify
} = module_map
# TODO: Match on anno directly in Elixir v1.22+
line =
case module_map do
%{anno: anno} -> :erl_anno.line(anno)
%{line: line} -> line
end
behaviours = for {:behaviour, module} <- attributes, do: module
no_warn_undefined =
compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify}
end
defp extract_no_warn_undefined(compile_opts) do
for(
{:no_warn_undefined, values} <- compile_opts,
@@ -400,18 +417,6 @@ defmodule Module.ParallelChecker do
_ -> %{}
end
defp maybe_module_map(binary, module) when is_binary(binary) do
# If a module was compiled without debug_info,
# then there is no module_map for further verification.
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
module_map
else
_ -> nil
end
end
defp cache_from_module_map(ets, map) do
exports =
[{:__info__, 1}] ++
@@ -419,6 +424,7 @@ defmodule Module.ParallelChecker do
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
cache_info(ets, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
module_map_to_module_tuple(map)
end
defp cache_info(ets, module, exports, deprecated, sigs, mode) do
+314 -82
View File
@@ -1,87 +1,284 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
alias Module.Types.{Descr, Expr, Pattern}
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
# perform subtyping and return its output (OUT).
#
# * :strict - Requires types signatures (not implemented).
# * Strong arrows with gradual performs subtyping and returns OUT
# * Weak arrows with gradual performs subtyping and returns OUT
#
# * :static - Type signatures have been given.
# * Strong arrows with gradual performs compatibility and returns OUT
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :dynamic - Type signatures have not been given.
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :infer - Same as :dynamic but skips remote calls.
#
# * :traversal - Focused mostly on traversing AST, skips most type system
# operations. Used by macros and when skipping inference.
#
# The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback).
@modes [:static, :dynamic, :infer, :traversal]
# These functions are not inferred because they are added/managed by the compiler
@no_infer [__protocol__: 1, behaviour_info: 1]
@doc false
def infer(module, file, defs, env) do
context = context()
def infer(module, file, defs, private, defmacrop, env) do
infer_signatures? = :elixir_config.get(:infer_signatures)
defmacrop = Map.from_keys(defmacrop, [])
for {{fun, arity}, :def, _meta, clauses} <- defs,
{fun, arity} not in @no_infer,
into: %{} do
stack = stack(:infer, file, module, {fun, arity}, :all, env)
expected = List.duplicate(Descr.dynamic(), arity)
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
false -> false
end
end
handler = fn meta, fun_arity, stack, context ->
case local_handler(meta, fun_arity, stack, context, finder) do
false ->
undefined_function!(:undefined_function, meta, fun_arity, stack, env)
false
{kind, _, _} = triplet ->
if (kind == :defmacro or kind == :defmacrop) and not Keyword.has_key?(meta, :super) do
undefined_function!(:incorrect_dispatch, meta, fun_arity, stack, env)
false
else
triplet
end
end
end
stack = stack(:infer, file, module, {:__info__, 1}, :all, env, handler)
{types, %{local_sigs: local_sigs}} =
for {fun_arity, kind, meta, clauses} = def <- defs, reduce: {[], context()} do
{types, context} ->
cond do
kind in [:def, :defmacro] ->
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], context}
else
{types, context}
end
kind == :defmacrop and is_map_key(defmacrop, fun_arity) ->
# Bypass the caching structure for defmacrop, that's because
# we don't need them stored in the signatures when we perform
# unreachable checks. This may cause defmacrop to be traversed
# twice if it uses default arguments (which is the only way
# to refer to another defmacrop in definitions) but that should
# be cheap anyway.
{_kind, _inferred, context} =
local_handler(fun_arity, kind, meta, clauses, :traversal, stack, context)
{types, context}
true ->
{types, context}
end
end
unreachable =
for {fun_arity, _kind, _meta, _defaults} = info <- private,
warn_unused_def(info, local_sigs, defmacrop, env),
not is_map_key(local_sigs, fun_arity),
do: fun_arity
{Map.new(types), unreachable}
end
defp infer_mode(kind, infer_signatures?) do
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
end
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
env = %{env | function: stack.function, file: stack.file}
tuple = {reason, {fun, arity}, stack.module}
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
end
defp warn_unused_def({_fun_arity, _kind, false, _}, _reachable, _used, _env) do
:ok
end
defp warn_unused_def({fun_arity, kind, meta, 0}, reachable, used, env) do
case is_map_key(reachable, fun_arity) or is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
:ok
end
defp warn_unused_def({tuple, kind, meta, default}, reachable, used, env) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, reachable, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
end
:ok
end
defp min_reachable_default(max, min, last, name, reachable, used) when max >= min do
fun_arity = {name, max}
case is_map_key(reachable, fun_arity) or is_map_key(used, fun_arity) do
true -> min_reachable_default(max - 1, min, max, name, reachable, used)
false -> min_reachable_default(max - 1, min, last, name, reachable, used)
end
end
defp min_reachable_default(_max, _min, last, _name, _reachable, _used) do
last
end
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause}
false -> false
end
end
handler = &local_handler(&1, &2, &3, &4, finder)
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
finder = fn _ -> {:dynamic, def} end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
context = warn_unused_clauses(defs, stack, context)
context.warnings
end
defp warn_unused_clauses(defs, stack, context) do
for {fun_arity, pending} <- context.local_used, pending != [], reduce: context do
context ->
{_fun_arity, kind, _meta, clauses} = List.keyfind(defs, fun_arity, 0)
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
clauses_indexes =
for type_index <- pending, {clause_index, ^type_index} <- mapping, do: clause_index
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
end
end
defp local_handler(_meta, fun_arity, stack, context, finder) do
case context.local_sigs do
%{^fun_arity => {kind, inferred, _mapping}} ->
{kind, inferred, context}
%{^fun_arity => kind} when is_atom(kind) ->
{kind, :none, context}
local_sigs ->
case finder.(fun_arity) do
{mode, {fun_arity, kind, meta, clauses}} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
{kind, inferred, context}
false ->
false
end
end
end
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
expected = List.duplicate(Descr.dynamic(), arity)
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
{_, _, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, [], [], context}, fn
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
context = fresh_context(context)
pair_types =
Enum.reduce(clauses, [], fn {meta, args, guards, body}, inferred ->
try do
{args, context} =
{args_types, context} =
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
{return, _context} = Expr.of_expr(body, stack, context)
add_inferred(inferred, args, return, [])
rescue
e -> internal_error!(e, __STACKTRACE__, :def, meta, module, fun, args, guards, body)
end
end)
{return_type, context} =
Expr.of_expr(body, stack, context)
# TODO: Reuse context from patterns and guards
{{fun, arity}, {:infer, Enum.reverse(pair_types)}}
end
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
if type_index == -1 do
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
else
{index + 1, total, [{index, type_index} | mapping], inferred, context}
end
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end)
inferred = {:infer, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(context, clauses_context)}
end
# We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, acc),
do: Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
defp add_inferred([head | tail], args, return, acc),
do: add_inferred(tail, args, return, [head | acc])
defp add_inferred([head | tail], args, return, index, acc),
do: add_inferred(tail, args, return, index - 1, [head | acc])
defp add_inferred([], args, return, acc),
do: [{args, return} | Enum.reverse(acc)]
defp add_inferred([], args, return, -1, acc),
do: {-1, [{args, return} | Enum.reverse(acc)]}
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
context = context()
Enum.flat_map(defs, fn {{fun, arity}, kind, meta, clauses} ->
file = with_file_meta(meta, file)
stack = stack(:dynamic, file, module, {fun, arity}, no_warn_undefined, cache)
expected = List.duplicate(Descr.dynamic(), arity)
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
try do
{_types, context} =
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
{_type, context} = Expr.of_expr(body, stack, context)
context.warnings
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, module, fun, args, guards, body)
end
end)
end)
end
defp with_file_meta(meta, file) do
defp with_file_meta(stack, meta) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> meta_file
:error -> file
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
:error -> stack
end
end
defp internal_error!(e, stack, kind, meta, module, fun, args, guards, body) do
defp internal_error!(e, trace, kind, meta, fun, args, guards, body, stack) do
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
exception =
RuntimeError.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, length(args))}:
found error while checking types for #{Exception.format_mfa(stack.module, fun, length(args))}:
#{Exception.format_banner(:error, e, stack)}\
@@ -92,7 +289,7 @@ defmodule Module.Types do
Please report this bug at: https://github.com/elixir-lang/elixir/issues
""")
reraise exception, stack
reraise exception, trace
end
defp guards_to_expr([], left) do
@@ -104,8 +301,8 @@ defmodule Module.Types do
end
@doc false
def stack(mode, file, module, function, no_warn_undefined, cache)
when mode in [:static, :dynamic, :infer] do
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
when mode in @modes do
%{
# The fallback meta used for literals in patterns and guards
meta: [],
@@ -119,29 +316,10 @@ defmodule Module.Types do
no_warn_undefined: no_warn_undefined,
# A tuple with cache information or a Macro.Env struct indicating no remote traversals
cache: cache,
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
# perform subtyping and return its output (OUT).
#
# * :strict - Requires types signatures (not implemented).
# * Strong arrows with gradual performs subtyping and returns OUT
# * Weak arrows with gradual performs subtyping and returns OUT
#
# * :static - Type signatures have been given.
# * Strong arrows with gradual performs compatibility and returns OUT
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :dynamic - Type signatures have not been given.
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :infer - Same as :dynamic but skips remote calls.
#
# The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback).
mode: mode
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
}
end
@@ -155,7 +333,61 @@ defmodule Module.Types do
# Variables and arguments from patterns
pattern_info: nil,
# If type checking has found an error/failure
failed: false
failed: false,
# Local signatures used by local handler
local_sigs: %{},
# Track which clauses have been used across private local calls
local_used: %{}
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
end
defp restore_context(%{vars: vars, failed: failed}, later_context) do
%{later_context | vars: vars, failed: failed}
end
## Diagnostics
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message: "this clause of #{kind} #{fun}/#{arity} is never used"
}
end
## Module errors
def format_error({:unused_args, {name, arity}}),
do: "default values for the optional arguments in #{name}/#{arity} are never used"
def format_error({:unused_args, {name, arity}, count}) when arity - count == 1,
do: "the default value for the last optional argument in #{name}/#{arity} is never used"
def format_error({:unused_args, {name, arity}, count}),
do:
"the default values for the last #{arity - count} optional arguments in #{name}/#{arity} are never used"
def format_error({:unused_def, {name, arity}, :defp}),
do: "function #{name}/#{arity} is unused"
def format_error({:unused_def, {name, arity}, :defmacrop}),
do: "macro #{name}/#{arity} is unused"
def format_error({:undefined_function, {f, a}, _})
when {f, a} in [__info__: 1, behaviour_info: 1, module_info: 1, module_info: 0],
do:
"undefined function #{f}/#{a} (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.#{f}/#{a})"
def format_error({:undefined_function, {f, a}, module}),
do:
"undefined function #{f}/#{a} (expected #{inspect(module)} to define such a function or for it to be imported, but none are available)"
def format_error({:incorrect_dispatch, {f, a}, _module}),
do: "cannot invoke macro #{f}/#{a} before its definition"
end
+955
View File
@@ -0,0 +1,955 @@
defmodule Module.Types.Apply do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
# We limit the size of the union for two reasons:
# To avoid really large outputs in reports and to
# reduce the computation cost of inferred code.
@max_clauses 16
alias Module.ParallelChecker
import Module.Types.{Helpers, Descr}
## Signatures
# Define strong arrows found in the standard library.
# A strong arrow means that, if a type outside of its
# domain is given, an error is raised. We are also
# ensuring that domains for the same function have
# no overlaps.
# Remote for callback info functions
kw = fn kw ->
kw
|> Enum.map(fn {key, type} when is_atom(key) ->
tuple([atom([key]), type])
end)
|> Enum.reduce(&union/2)
|> list()
end
fas = list(tuple([atom(), integer()]))
shared_info = [
attributes: list(tuple([atom(), list(term())])),
compile: kw.(version: list(integer()), source: list(integer()), options: list(term())),
exports: fas,
md5: binary(),
module: atom()
]
infos =
%{
behaviour_info: [
callbacks: fas,
optional_callbacks: fas
],
module_info: [functions: fas, nifs: fas] ++ shared_info,
__info__:
[
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
exports_md5: binary(),
functions: fas,
macros: fas,
struct:
list(closed_map(default: term(), field: atom(), required: boolean()))
|> union(atom([nil]))
] ++ shared_info,
__protocol__: [
module: atom(),
functions: fas,
consolidated?: boolean(),
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))
]
}
for {name, clauses} <- infos do
domain = atom(Keyword.keys(clauses))
clauses = Enum.map(clauses, fn {key, return} -> {[atom([key])], return} end)
defp signature(unquote(name), 1) do
{:strong, [unquote(Macro.escape(domain))], unquote(Macro.escape(clauses))}
end
end
defp signature(:module_info, 0) do
{:strong, nil, [{[], unquote(Macro.escape(kw.(infos.module_info)))}]}
end
defp signature(_, _), do: :none
# Remote for compiler functions
mfargs = [atom(), atom(), list(term())]
send_destination =
pid()
|> union(reference())
|> union(port())
|> union(atom())
|> union(tuple([atom(), atom()]))
basic_arith_2_args_clauses = [
{[integer(), integer()], integer()},
{[integer(), float()], float()},
{[float(), integer()], float()},
{[float(), float()], float()}
]
is_clauses = [{[term()], boolean()}]
args_or_arity = union(list(term()), integer())
args_or_none = union(list(term()), atom([:none]))
extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
raise_stacktrace =
list(
tuple([atom(), atom(), args_or_arity, extra_info])
|> union(tuple([atom(), atom(), args_or_arity]))
|> union(tuple([fun(), args_or_arity, extra_info]))
|> union(tuple([fun(), args_or_arity]))
)
and_signature =
for left <- [true, false], right <- [true, false] do
{[atom([left]), atom([right])], atom([left and right])}
end
or_signature =
for left <- [true, false], right <- [true, false] do
{[atom([left]), atom([right])], atom([left or right])}
end
for {mod, fun, clauses} <- [
# :binary
{:binary, :copy, [{[binary(), integer()], binary()}]},
# :erlang
{:erlang, :+, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :+, basic_arith_2_args_clauses},
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :-, basic_arith_2_args_clauses},
{:erlang, :*, basic_arith_2_args_clauses},
{:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
{:erlang, :"/=", [{[term(), term()], boolean()}]},
{:erlang, :"=/=", [{[term(), term()], boolean()}]},
{:erlang, :<, [{[term(), term()], boolean()}]},
{:erlang, :"=<", [{[term(), term()], boolean()}]},
{:erlang, :==, [{[term(), term()], boolean()}]},
{:erlang, :"=:=", [{[term(), term()], boolean()}]},
{:erlang, :>, [{[term(), term()], boolean()}]},
{:erlang, :>=, [{[term(), term()], boolean()}]},
{:erlang, :abs, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :and, and_signature},
{:erlang, :atom_to_binary, [{[atom()], binary()}]},
{:erlang, :atom_to_list, [{[atom()], list(integer())}]},
{:erlang, :band, [{[integer(), integer()], integer()}]},
{:erlang, :binary_part, [{[binary(), integer(), integer()], binary()}]},
{:erlang, :binary_to_atom, [{[binary()], atom()}]},
{:erlang, :binary_to_existing_atom, [{[binary()], atom()}]},
{:erlang, :binary_to_integer, [{[binary()], integer()}]},
{:erlang, :binary_to_integer, [{[binary(), integer()], integer()}]},
{:erlang, :binary_to_float, [{[binary()], float()}]},
{:erlang, :bit_size, [{[binary()], integer()}]},
{:erlang, :bnot, [{[integer()], integer()}]},
{:erlang, :bor, [{[integer(), integer()], integer()}]},
{:erlang, :bsl, [{[integer(), integer()], integer()}]},
{:erlang, :bsr, [{[integer(), integer()], integer()}]},
{:erlang, :bxor, [{[integer(), integer()], integer()}]},
{:erlang, :byte_size, [{[binary()], integer()}]},
{:erlang, :ceil, [{[union(integer(), float())], integer()}]},
{:erlang, :div, [{[integer(), integer()], integer()}]},
{:erlang, :error, [{[term()], none()}]},
{:erlang, :error, [{[term(), args_or_none], none()}]},
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
{:erlang, :floor, [{[union(integer(), float())], integer()}]},
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
{:erlang, :integer_to_binary, [{[integer()], binary()}]},
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
{:erlang, :integer_to_list, [{[integer()], non_empty_list(integer())}]},
{:erlang, :integer_to_list, [{[integer(), integer()], non_empty_list(integer())}]},
{:erlang, :is_atom, is_clauses},
{:erlang, :is_binary, is_clauses},
{:erlang, :is_bitstring, is_clauses},
{:erlang, :is_boolean, is_clauses},
{:erlang, :is_float, is_clauses},
{:erlang, :is_function, is_clauses},
{:erlang, :is_function, [{[term(), integer()], boolean()}]},
{:erlang, :is_integer, is_clauses},
{:erlang, :is_list, is_clauses},
{:erlang, :is_map, is_clauses},
{:erlang, :is_map_key, [{[term(), open_map()], boolean()}]},
{:erlang, :is_number, is_clauses},
{:erlang, :is_pid, is_clauses},
{:erlang, :is_port, is_clauses},
{:erlang, :is_reference, is_clauses},
{:erlang, :is_tuple, is_clauses},
{:erlang, :length, [{[list(term())], integer()}]},
{:erlang, :list_to_atom, [{[list(integer())], atom()}]},
{:erlang, :list_to_existing_atom, [{[list(integer())], atom()}]},
{:erlang, :list_to_float, [{[non_empty_list(integer())], float()}]},
{:erlang, :list_to_integer, [{[non_empty_list(integer())], integer()}]},
{:erlang, :list_to_integer, [{[non_empty_list(integer()), integer()], integer()}]},
{:erlang, :make_ref, [{[], reference()}]},
{:erlang, :map_size, [{[open_map()], integer()}]},
{:erlang, :node, [{[], atom()}]},
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
{:erlang, :not, [{[atom([false])], atom([true])}, {[atom([true])], atom([false])}]},
{:erlang, :or, or_signature},
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
{:erlang, :rem, [{[integer(), integer()], integer()}]},
{:erlang, :round, [{[union(integer(), float())], integer()}]},
{:erlang, :self, [{[], pid()}]},
{:erlang, :spawn, [{[fun()], pid()}]},
{:erlang, :spawn, [{mfargs, pid()}]},
{:erlang, :spawn_link, [{[fun()], pid()}]},
{:erlang, :spawn_link, [{mfargs, pid()}]},
{:erlang, :spawn_monitor, [{[fun()], tuple([reference(), pid()])}]},
{:erlang, :spawn_monitor, [{mfargs, tuple([reference(), pid()])}]},
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
# TODO: Replace term()/dynamic() by parametric types
{:erlang, :++, [{[list(term()), term()], dynamic(list(term(), term()))}]},
{:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]},
{:erlang, :andalso, [{[boolean(), term()], dynamic()}]},
{:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]},
{:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]},
{:erlang, :insert_element,
[{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
{:erlang, :max, [{[term(), term()], dynamic()}]},
{:erlang, :min, [{[term(), term()], dynamic()}]},
{:erlang, :orelse, [{[boolean(), term()], dynamic()}]},
{:erlang, :send, [{[send_destination, term()], dynamic()}]},
{:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]}
] do
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
true =
Code.ensure_loaded?(mod) and
(function_exported?(mod, fun, arity) or fun in [:orelse, :andalso])
domain_clauses =
case clauses do
[_] ->
{:strong, nil, clauses}
_ ->
domain =
clauses
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
{:strong, domain, clauses}
end
defp signature(unquote(mod), unquote(fun), unquote(arity)),
do: unquote(Macro.escape(domain_clauses))
end
defp signature(_mod, _fun, _arity), do: :none
@doc """
Applies a function in unknown modules.
Used only by info functions.
"""
def remote(_name, _args_types, _expr, %{mode: :traversal}, context) do
{dynamic(), context}
end
def remote(name, args_types, expr, stack, context) do
arity = length(args_types)
case signature(name, arity) do
:none -> {dynamic(), context}
info -> apply_remote(info, args_types, expr, stack, context)
end
end
@doc """
Applies a function in a given module.
"""
def remote(_module, _fun, _args_types, _expr, %{mode: :traversal}, context) do
{dynamic(), context}
end
def remote(:erlang, :element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
when is_integer(index) do
case tuple_fetch(tuple, index - 1) do
{_optional?, value_type} ->
{value_type, context}
:badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)}
reason ->
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
end
end
def remote(
:erlang,
:insert_element,
[_, tuple, value],
{_, meta, [index, _, _]} = expr,
stack,
context
)
when is_integer(index) do
case tuple_insert_at(tuple, index - 1, value) do
value_type when is_descr(value_type) ->
{value_type, context}
:badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple, value], stack, context)}
reason ->
{error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)}
end
end
def remote(:erlang, :delete_element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
when is_integer(index) do
case tuple_delete_at(tuple, index - 1) do
value_type when is_descr(value_type) ->
{value_type, context}
:badtuple ->
{error_type(), badremote_error(expr, [integer(), tuple], stack, context)}
reason ->
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
end
end
def remote(:erlang, :make_tuple, [_, elem], {_, _meta, [size, _]}, _stack, context)
when is_integer(size) and size >= 0 do
{tuple(List.duplicate(elem, size)), context}
end
def remote(:erlang, :hd, [list], expr, stack, context) do
case list_hd(list) do
{_, value_type} ->
{value_type, context}
:badnonemptylist ->
{error_type(), badremote_error(expr, [list], stack, context)}
end
end
def remote(:erlang, :tl, [list], expr, stack, context) do
case list_tl(list) do
{_, value_type} ->
{value_type, context}
:badnonemptylist ->
{error_type(), badremote_error(expr, [list], stack, context)}
end
end
def remote(:erlang, name, [left, right] = args_types, expr, stack, context)
when name in [:>=, :"=<", :>, :<, :min, :max] do
context =
cond do
stack.mode == :infer ->
context
match?({false, _}, map_fetch(left, :__struct__)) or
match?({false, _}, map_fetch(right, :__struct__)) ->
warning = {:struct_comparison, expr, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
number_type?(left) and number_type?(right) ->
context
disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
true ->
context
end
if name in [:min, :max] do
{union(left, right), context}
else
{return(boolean(), args_types, stack), context}
end
end
def remote(:erlang, name, [left, right] = args_types, expr, stack, context)
when name in [:==, :"/=", :"=:=", :"=/="] do
context =
cond do
stack.mode == :infer ->
context
name in [:==, :"/="] and number_type?(left) and number_type?(right) ->
context
disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
true ->
context
end
{return(boolean(), args_types, stack), context}
end
def remote(mod, name, args_types, expr, stack, context) do
arity = length(args_types)
case :elixir_rewrite.inline(mod, name, arity) do
{mod, name} ->
remote(mod, name, args_types, expr, stack, context)
false ->
{info, context} = signature(mod, name, arity, elem(expr, 1), stack, context)
apply_remote(info, args_types, expr, stack, context)
end
end
defp apply_remote(info, args_types, expr, stack, context) do
case apply_signature(info, args_types, stack) do
{:ok, _indexes, type} ->
{type, context}
{:error, domain, clauses} ->
error = {:badremote, expr, args_types, domain, clauses, context}
{error_type(), error(error, elem(expr, 1), stack, context)}
end
end
@doc """
Gets a mfa signature.
It returns either a tuple with the remote information and the context.
The remote information may be one of:
* `:none` - no typing information found.
* `{:infer, clauses}` - clauses from inferences. You must check all
all clauses and return the union between them. They are dynamic
and they can only be converted into arrows by computing the union
of all arguments.
* `{:strong, domain or nil, clauses}` - clauses from signatures. So far
these are strong arrows with non-overlapping domains
"""
def signature(module, fun, arity, meta, stack, context) when is_atom(module) do
if Keyword.get(meta, :runtime_module, false) do
{:none, context}
else
case signature(module, fun, arity) do
:none -> export(module, fun, arity, meta, stack, context)
clauses -> {clauses, context}
end
end
end
defp export(_module, :module_info, arity, _meta, _stack, context) when arity in [0, 1] do
{signature(:module_info, arity), context}
end
defp export(_module, _fun, _arity, _meta, %{cache: %Macro.Env{}}, context) do
{:none, context}
end
defp export(module, fun, arity, meta, stack, context) do
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
{:ok, mode, reason, info} ->
info = if info == :none, do: signature(fun, arity), else: info
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
{:error, type} ->
context =
if warn_undefined?(module, fun, arity, stack) do
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
else
context
end
{:none, context}
end
end
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
if reason do
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
reason = string |> List.to_string() |> :string.titlecase()
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
reason = string |> List.to_string() |> :string.titlecase()
reason = "It will be removed in #{removal}. #{reason}"
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
_ ->
context
end
end
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, stack) do
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
## Local
@doc """
Deal with local functions.
"""
def local(fun, args_types, {_, meta, _} = expr, stack, context) do
fun_arity = {fun, length(args_types)}
case stack.local_handler.(meta, fun_arity, stack, context) do
false ->
{dynamic(), context}
{_kind, _info, context} when stack.mode == :traversal ->
{dynamic(), context}
{kind, info, context} ->
case apply_signature(info, args_types, stack) do
{:ok, indexes, type} ->
context =
if stack.mode != :infer and kind == :defp do
update_in(context.local_used[fun_arity], fn current ->
if info == :none do
[]
else
(current || used_from_clauses(info)) -- indexes
end
end)
else
context
end
{type, context}
{:error, domain, clauses} ->
error = {:badlocal, expr, args_types, domain, clauses, context}
{error_type(), error(error, with_span(meta, fun), stack, context)}
end
end
end
defp used_from_clauses({:infer, clauses}),
do: Enum.with_index(clauses, fn _, i -> i end)
defp used_from_clauses({:strong, _, clauses}),
do: Enum.with_index(clauses, fn _, i -> i end)
@doc """
Deal with local captures.
"""
def local_capture(fun, arity, meta, stack, context) do
fun_arity = {fun, arity}
case stack.local_handler.(meta, fun_arity, stack, context) do
false ->
{dynamic(fun()), context}
{_kind, _info, context} when stack.mode == :traversal ->
{fun(), context}
{kind, _info, context} ->
if stack.mode != :infer and kind == :defp do
# Mark all clauses as used, as the function is being exported.
{fun(), put_in(context.local_used[fun_arity], [])}
else
{fun(), context}
end
end
end
## Application helpers
defp return(type, args_types, stack) do
cond do
stack.mode == :static -> type
Enum.any?(args_types, &gradual?/1) -> dynamic(type)
true -> type
end
end
defp apply_signature(:none, _args_types, _stack) do
{:ok, [], dynamic()}
end
defp apply_signature({:strong, nil, [{expected, return}] = clauses}, args_types, stack) do
# Optimize single clauses as the domain is the single clause args.
case zip_compatible?(args_types, expected) do
true -> {:ok, [0], return(return, args_types, stack)}
false -> {:error, expected, clauses}
end
end
defp apply_signature({:strong, domain, clauses}, args_types, stack) do
# If the type is only gradual, the compatibility check is the same
# as a non disjoint check. So we skip checking compatibility twice.
with true <- zip_compatible_or_only_gradual?(args_types, domain),
{count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do
{:ok, used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)}
else
_ -> {:error, domain, clauses}
end
end
defp apply_signature({:infer, clauses}, args_types, _stack) do
case apply_clauses(clauses, args_types, 0, 0, [], []) do
{0, [], []} ->
domain =
clauses
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
{:error, domain, clauses}
{count, used, _returns} when count > @max_clauses ->
{:ok, used, dynamic()}
{_count, used, returns} ->
{:ok, used, returns |> Enum.reduce(&union/2) |> dynamic()}
end
end
defp apply_clauses([{expected, return} | clauses], args_types, index, count, used, returns) do
if zip_not_disjoint?(args_types, expected) do
apply_clauses(clauses, args_types, index + 1, count + 1, [index | used], [return | returns])
else
apply_clauses(clauses, args_types, index + 1, count, used, returns)
end
end
defp apply_clauses([], _args_types, _index, count, used, returns) do
{count, used, returns}
end
defp zip_compatible_or_only_gradual?([actual | actuals], [expected | expecteds]) do
(only_gradual?(actual) or compatible?(actual, expected)) and
zip_compatible_or_only_gradual?(actuals, expecteds)
end
defp zip_compatible_or_only_gradual?([], []), do: true
defp zip_compatible?([actual | actuals], [expected | expecteds]) do
compatible?(actual, expected) and zip_compatible?(actuals, expecteds)
end
defp zip_compatible?([], []), do: true
defp zip_not_disjoint?([actual | actuals], [expected | expecteds]) do
not disjoint?(actual, expected) and zip_not_disjoint?(actuals, expecteds)
end
defp zip_not_disjoint?([], []), do: true
## Error handling
defp error(warning, meta, stack, context) do
error(__MODULE__, warning, meta, stack, context)
end
defp badremote_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do
{_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, length(args_types))
error({:badremote, expr, args_types, domain || args, clauses, context}, meta, stack, context)
end
## Diagnosstics
def format_diagnostic({:badindex, expr, type, index, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{format_mfa(expr)}:
#{expr_to_string(expr) |> indent(4)}
the given type does not have the given index:
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces)
])
}
end
def format_diagnostic({:badlocal, expr, args_types, domain, clauses, context}) do
traces = collect_traces(expr, context)
converter = &Function.identity/1
{fun, _, _} = expr
explanation =
empty_arg_reason(args_types) ||
"""
but expected one of:
#{clauses_args_to_quoted_string(clauses, converter)}
"""
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types given to #{fun}/#{length(args_types)}:
#{expr_to_string(expr) |> indent(4)}
given types:
#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}
""",
explanation,
format_traces(traces)
])
}
end
def format_diagnostic({:badremote, expr, args_types, domain, clauses, context}) do
traces = collect_traces(expr, context)
{{:., _, [mod, fun]}, _, args} = expr
{mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args)
explanation =
empty_arg_reason(converter.(args_types)) ||
"""
but expected one of:
#{clauses_args_to_quoted_string(clauses, converter)}
"""
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types given to #{Exception.format_mfa(mod, fun, length(args))}:
#{expr_to_string(expr) |> indent(4)}
given types:
#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}
""",
explanation,
format_traces(traces)
])
}
end
def format_diagnostic({:mismatched_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison between distinct types found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
While Elixir can compare across all types, you are comparing \
across types which are always disjoint, and the result is either \
always true or always false
"""
])
}
end
def format_diagnostic({:struct_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Structs that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison.
"""
])
}
end
def format_diagnostic({:undefined, :module, module, fun, arity}) do
top =
if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}"
else
Exception.format_mfa(module, fun, arity)
end
%{
message:
IO.iodata_to_binary([
top,
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)",
UndefinedFunctionError.hint_for_missing_module(module, fun, arity)
]),
group: true
}
end
def format_diagnostic({:undefined, :function, module, :__struct__, 0}) do
%{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
group: true
}
end
def format_diagnostic({:undefined, :function, module, fun, arity}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity)
]),
group: true
}
end
def format_diagnostic({:deprecated, module, fun, arity, reason}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]),
group: true
}
end
defp empty_arg_reason(args_types) do
if i = Enum.find_index(args_types, &empty?/1) do
"""
the #{integer_to_ordinal(i + 1)} argument is empty (often represented as none()), \
most likely because it is the result of an expression that always fails, such as \
a `raise` or a previous invalid call. This causes any function called with this \
value to fail
"""
end
end
defp pluralize(1, singular, _), do: "1 #{singular}"
defp pluralize(i, _, plural), do: "#{i} #{plural}"
defp format_mfa({{:., _, [mod, fun]}, _, args}) do
{mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args)
Exception.format_mfa(mod, fun, length(args))
end
## Algebra helpers
alias Inspect.Algebra, as: IA
defp clauses_args_to_quoted_string([{args, _return}], converter) do
"\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4))
end
defp clauses_args_to_quoted_string(clauses, converter) do
clauses
|> Enum.with_index(fn {args, _return}, index ->
"""
##{index + 1}
#{clause_args_to_quoted_string(args, converter)}\
"""
|> indent(4)
end)
|> Enum.join("\n")
end
defp clause_args_to_quoted_string(args, converter) do
docs = Enum.map(args, &(&1 |> to_quoted() |> Code.Formatter.to_algebra()))
args_docs_to_quoted_string(converter.(docs))
end
defp args_to_quoted_string(args_types, domain, converter) do
ansi? = IO.ANSI.enabled?()
docs =
Enum.zip_with(args_types, domain, fn actual, expected ->
doc = actual |> to_quoted() |> Code.Formatter.to_algebra()
cond do
compatible?(actual, expected) -> doc
ansi? -> IA.concat(IA.color(doc, IO.ANSI.red()), IA.color(IA.empty(), IO.ANSI.reset()))
true -> IA.concat(["-", doc, "-"])
end
end)
args_docs_to_quoted_string(converter.(docs))
end
defp args_docs_to_quoted_string(docs) do
doc = IA.fold(docs, fn doc, acc -> IA.glue(IA.concat(doc, ","), acc) end)
wrapped_docs =
case docs do
[_] -> IA.concat("(", IA.concat(doc, ")"))
_ -> IA.group(IA.glue(IA.nest(IA.glue("(", "", doc), 2), "", ")"))
end
wrapped_docs
|> IA.format(98)
|> IO.iodata_to_binary()
|> case do
"(\n" <> _ = multiple_lines -> multiple_lines
single_line -> binary_slice(single_line, 1..-2//1)
end
end
defp integer_to_ordinal(i) do
case rem(i, 10) do
1 when rem(i, 100) != 11 -> "#{i}st"
2 when rem(i, 100) != 12 -> "#{i}nd"
3 when rem(i, 100) != 13 -> "#{i}rd"
_ -> "#{i}th"
end
end
end
+65 -30
View File
@@ -1,7 +1,7 @@
defmodule Module.Types.Expr do
@moduledoc false
alias Module.Types.{Of, Pattern}
alias Module.Types.{Apply, Of, Pattern}
import Module.Types.{Helpers, Descr}
14 = length(Macro.Env.__info__(:struct))
@@ -78,14 +78,35 @@ defmodule Module.Types.Expr do
{prefix, suffix} = unpack_list(list, [])
{prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, stack, &2))
{suffix, context} = of_expr(suffix, stack, context)
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
if stack.mode == :traversal do
{dynamic(), context}
else
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
end
end
# {left, right}
def of_expr({left, right}, stack, context) do
{left, context} = of_expr(left, stack, context)
{right, context} = of_expr(right, stack, context)
{tuple([left, right]), context}
if stack.mode == :traversal do
{dynamic(), context}
else
{tuple([left, right]), context}
end
end
# {...}
def of_expr({:{}, _meta, exprs}, stack, context) do
{types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, stack, &2))
if stack.mode == :traversal do
{dynamic(), context}
else
{tuple(types), context}
end
end
# <<...>>>
@@ -103,12 +124,6 @@ defmodule Module.Types.Expr do
{@stacktrace, context}
end
# {...}
def of_expr({:{}, _meta, exprs}, stack, context) do
{types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, stack, &2))
{tuple(types), context}
end
# left = right
def of_expr({:=, _, [left_expr, right_expr]} = expr, stack, context) do
{left_expr, right_expr} = repack_match(left_expr, right_expr)
@@ -221,7 +236,7 @@ defmodule Module.Types.Expr do
{head_type, context} = of_expr(head, stack, context)
context =
if stack.mode == :infer do
if stack.mode in [:infer, :traversal] do
context
else
case truthness(head_type) do
@@ -386,32 +401,47 @@ defmodule Module.Types.Expr do
)
when is_atom(name) and is_integer(arity) do
{remote_type, context} = of_expr(remote, stack, context)
# TODO: We cannot return the unions of functions. Do we forbid this?
# Do we check it is always the same return type? Do we simply say it is a function?
{mods, context} = Of.modules(remote_type, name, arity, expr, meta, stack, context)
context =
Enum.reduce(mods, context, &(Of.remote(&1, name, arity, meta, stack, &2) |> elem(1)))
Enum.reduce(
mods,
context,
&(Apply.signature(&1, name, arity, meta, stack, &2) |> elem(1))
)
{fun(), context}
{dynamic(fun()), context}
end
# &foo/1
# TODO: & &1
def of_expr({:&, _meta, _arg}, _stack, context) do
{fun(), context}
# TODO: &foo/1
def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, stack, context) do
Apply.local_capture(fun, arity, meta, stack, context)
end
# TODO: local_call(arg)
def of_expr({fun, _meta, args}, stack, context)
# Super
def of_expr({:super, meta, args} = expr, stack, context) when is_list(args) do
{_kind, fun} = Keyword.fetch!(meta, :super)
{args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2))
Apply.local(fun, args_types, expr, stack, context)
end
# Local calls
def of_expr({fun, _meta, args} = expr, stack, context)
when is_atom(fun) and is_list(args) do
{_arg_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2))
{dynamic(), context}
{args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2))
Apply.local(fun, args_types, expr, stack, context)
end
# var
def of_expr(var, _stack, context) when is_var(var) do
{Of.var(var, context), context}
def of_expr(var, stack, context) when is_var(var) do
if stack.mode == :traversal do
{dynamic(), context}
else
{Of.var(var, context), context}
end
end
## Try
@@ -427,9 +457,8 @@ defmodule Module.Types.Expr do
{info, context} = Of.struct_info(exception, meta, stack, context)
{Of.struct_type(exception, info, args), context}
else
# If the exception cannot be found or is invalid,
# we call Of.remote/5 to emit a warning.
{_, context} = Of.remote(exception, :__struct__, 0, meta, stack, context)
# If the exception cannot be found or is invalid, fetch the signature to emit warnings.
{_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context)
{error_type(), context}
end
end)
@@ -521,17 +550,17 @@ defmodule Module.Types.Expr do
## General helpers
defp apply_many([], function, args_types, expr, stack, context) do
Of.apply(function, args_types, expr, stack, context)
Apply.remote(function, args_types, expr, stack, context)
end
defp apply_many([mod], function, args_types, expr, stack, context) do
Of.apply(mod, function, args_types, expr, stack, context)
Apply.remote(mod, function, args_types, expr, stack, context)
end
defp apply_many(mods, function, args_types, expr, stack, context) do
{returns, context} =
Enum.map_reduce(mods, context, fn mod, context ->
Of.apply(mod, function, args_types, expr, stack, context)
Apply.remote(mod, function, args_types, expr, stack, context)
end)
{Enum.reduce(returns, &union/2), context}
@@ -546,7 +575,7 @@ defmodule Module.Types.Expr do
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
defp of_clauses(clauses, expected, info, stack, {acc, context}) do
defp of_clauses(clauses, expected, info, %{mode: mode} = stack, {acc, context}) do
%{failed: failed?} = context
Enum.reduce(clauses, {acc, context}, fn {:->, meta, [head, body]}, {acc, context} ->
@@ -554,7 +583,13 @@ defmodule Module.Types.Expr do
{patterns, guards} = extract_head(head)
{_types, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context)
{body, context} = of_expr(body, stack, context)
{union(acc, body), set_failed(context, failed?)}
context = set_failed(context, failed?)
if mode == :traversal do
{dynamic(), context}
else
{union(acc, body), context}
end
end)
end
+7
View File
@@ -38,6 +38,13 @@ defmodule Module.Types.Helpers do
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Attaches span information.
"""
def with_span(meta, name) do
:elixir_env.calculate_span(meta, name)
end
## Warnings
@doc """
+38 -834
View File
@@ -2,8 +2,6 @@ defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
alias Module.ParallelChecker
import Module.Types.{Helpers, Descr}
@prefix quote(do: ...)
@@ -117,6 +115,17 @@ defmodule Module.Types.Of do
@doc """
Builds permutation of maps according to the given keys.
"""
def permutate_map(pairs, %{mode: :traversal} = stack, context, of_fun, _of_map) do
context =
Enum.reduce(pairs, context, fn {key, value}, context ->
{_, context} = of_fun.(key, stack, context)
{_, context} = of_fun.(value, stack, context)
context
end)
{dynamic(), context}
end
def permutate_map(pairs, stack, context, of_fun, of_map) do
{dynamic?, fallback, single, multiple, assert, context} =
Enum.reduce(pairs, {false, none(), [], [], [], context}, fn
@@ -215,7 +224,8 @@ defmodule Module.Types.Of do
end
_ ->
{_, context} = export(struct, :__struct__, 0, meta, stack, context)
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
@@ -333,7 +343,7 @@ defmodule Module.Types.Of do
## Modules
@doc """
Returns the modules.
Returns modules in a type.
The call information is used on report reporting.
"""
@@ -348,568 +358,6 @@ defmodule Module.Types.Of do
end
end
## Remotes
# Define strong arrows found in the standard library.
# A strong arrow means that, if a type outside of its
# domain is given, an error is raised. We are also
# ensuring that domains for the same function have
# no overlaps.
# Remote for callback info functions
kw = fn kw ->
kw
|> Enum.map(fn {key, type} when is_atom(key) ->
tuple([atom([key]), type])
end)
|> Enum.reduce(&union/2)
|> list()
end
fas = list(tuple([atom(), integer()]))
shared_info = [
attributes: list(tuple([atom(), list(term())])),
compile: kw.(version: list(integer()), source: list(integer()), options: list(term())),
exports: fas,
md5: binary(),
module: atom()
]
infos =
%{
behaviour_info: [
callbacks: fas,
optional_callbacks: fas
],
module_info: [functions: fas, nifs: fas] ++ shared_info,
__info__:
[
deprecated: list(tuple([tuple([atom(), integer()]), binary()])),
exports_md5: binary(),
functions: fas,
macros: fas,
struct:
list(closed_map(default: term(), field: atom(), required: boolean()))
|> union(atom([nil]))
] ++ shared_info,
__protocol__: [
module: atom(),
functions: fas,
consolidated?: boolean(),
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))
]
}
for {name, clauses} <- infos do
domain = atom(Keyword.keys(clauses))
clauses = Enum.map(clauses, fn {key, return} -> {[atom([key])], return} end)
defp remote(unquote(name), 1) do
{:strong, [unquote(Macro.escape(domain))], unquote(Macro.escape(clauses))}
end
end
defp remote(:module_info, 0) do
{:strong, nil, [{[], unquote(Macro.escape(kw.(infos.module_info)))}]}
end
defp remote(_, _), do: :none
# Remote for compiler functions
mfargs = [atom(), atom(), list(term())]
send_destination =
pid()
|> union(reference())
|> union(port())
|> union(atom())
|> union(tuple([atom(), atom()]))
basic_arith_2_args_clauses = [
{[integer(), integer()], integer()},
{[integer(), float()], float()},
{[float(), integer()], float()},
{[float(), float()], float()}
]
is_clauses = [{[term()], boolean()}]
args_or_arity = union(list(term()), integer())
args_or_none = union(list(term()), atom([:none]))
extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
raise_stacktrace =
list(
tuple([atom(), atom(), args_or_arity, extra_info])
|> union(tuple([atom(), atom(), args_or_arity]))
|> union(tuple([fun(), args_or_arity, extra_info]))
|> union(tuple([fun(), args_or_arity]))
)
and_signature =
for left <- [true, false], right <- [true, false] do
{[atom([left]), atom([right])], atom([left and right])}
end
or_signature =
for left <- [true, false], right <- [true, false] do
{[atom([left]), atom([right])], atom([left or right])}
end
for {mod, fun, clauses} <- [
# :binary
{:binary, :copy, [{[binary(), integer()], binary()}]},
# :erlang
{:erlang, :+, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :+, basic_arith_2_args_clauses},
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :-, basic_arith_2_args_clauses},
{:erlang, :*, basic_arith_2_args_clauses},
{:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
{:erlang, :"/=", [{[term(), term()], boolean()}]},
{:erlang, :"=/=", [{[term(), term()], boolean()}]},
{:erlang, :<, [{[term(), term()], boolean()}]},
{:erlang, :"=<", [{[term(), term()], boolean()}]},
{:erlang, :==, [{[term(), term()], boolean()}]},
{:erlang, :"=:=", [{[term(), term()], boolean()}]},
{:erlang, :>, [{[term(), term()], boolean()}]},
{:erlang, :>=, [{[term(), term()], boolean()}]},
{:erlang, :abs, [{[integer()], integer()}, {[float()], float()}]},
{:erlang, :and, and_signature},
{:erlang, :atom_to_binary, [{[atom()], binary()}]},
{:erlang, :atom_to_list, [{[atom()], list(integer())}]},
{:erlang, :band, [{[integer(), integer()], integer()}]},
{:erlang, :binary_part, [{[binary(), integer(), integer()], binary()}]},
{:erlang, :binary_to_atom, [{[binary()], atom()}]},
{:erlang, :binary_to_existing_atom, [{[binary()], atom()}]},
{:erlang, :binary_to_integer, [{[binary()], integer()}]},
{:erlang, :binary_to_integer, [{[binary(), integer()], integer()}]},
{:erlang, :binary_to_float, [{[binary()], float()}]},
{:erlang, :bit_size, [{[binary()], integer()}]},
{:erlang, :bnot, [{[integer()], integer()}]},
{:erlang, :bor, [{[integer(), integer()], integer()}]},
{:erlang, :bsl, [{[integer(), integer()], integer()}]},
{:erlang, :bsr, [{[integer(), integer()], integer()}]},
{:erlang, :bxor, [{[integer(), integer()], integer()}]},
{:erlang, :byte_size, [{[binary()], integer()}]},
{:erlang, :ceil, [{[union(integer(), float())], integer()}]},
{:erlang, :div, [{[integer(), integer()], integer()}]},
{:erlang, :error, [{[term()], none()}]},
{:erlang, :error, [{[term(), args_or_none], none()}]},
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
{:erlang, :floor, [{[union(integer(), float())], integer()}]},
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
{:erlang, :integer_to_binary, [{[integer()], binary()}]},
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
{:erlang, :integer_to_list, [{[integer()], non_empty_list(integer())}]},
{:erlang, :integer_to_list, [{[integer(), integer()], non_empty_list(integer())}]},
{:erlang, :is_atom, is_clauses},
{:erlang, :is_binary, is_clauses},
{:erlang, :is_bitstring, is_clauses},
{:erlang, :is_boolean, is_clauses},
{:erlang, :is_float, is_clauses},
{:erlang, :is_function, is_clauses},
{:erlang, :is_function, [{[term(), integer()], boolean()}]},
{:erlang, :is_integer, is_clauses},
{:erlang, :is_list, is_clauses},
{:erlang, :is_map, is_clauses},
{:erlang, :is_map_key, [{[term(), open_map()], boolean()}]},
{:erlang, :is_number, is_clauses},
{:erlang, :is_pid, is_clauses},
{:erlang, :is_port, is_clauses},
{:erlang, :is_reference, is_clauses},
{:erlang, :is_tuple, is_clauses},
{:erlang, :length, [{[list(term())], integer()}]},
{:erlang, :list_to_atom, [{[list(integer())], atom()}]},
{:erlang, :list_to_existing_atom, [{[list(integer())], atom()}]},
{:erlang, :list_to_float, [{[non_empty_list(integer())], float()}]},
{:erlang, :list_to_integer, [{[non_empty_list(integer())], integer()}]},
{:erlang, :list_to_integer, [{[non_empty_list(integer()), integer()], integer()}]},
{:erlang, :list_to_tuple, [{[list(term())], dynamic(open_tuple([]))}]},
{:erlang, :make_ref, [{[], reference()}]},
{:erlang, :map_size, [{[open_map()], integer()}]},
{:erlang, :node, [{[], atom()}]},
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
{:erlang, :not, [{[atom([false])], atom([true])}, {[atom([true])], atom([false])}]},
{:erlang, :or, or_signature},
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
{:erlang, :rem, [{[integer(), integer()], integer()}]},
{:erlang, :round, [{[union(integer(), float())], integer()}]},
{:erlang, :self, [{[], pid()}]},
{:erlang, :spawn, [{[fun()], pid()}]},
{:erlang, :spawn, [{mfargs, pid()}]},
{:erlang, :spawn_link, [{[fun()], pid()}]},
{:erlang, :spawn_link, [{mfargs, pid()}]},
{:erlang, :spawn_monitor, [{[fun()], tuple([reference(), pid()])}]},
{:erlang, :spawn_monitor, [{mfargs, tuple([reference(), pid()])}]},
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
# TODO: Replace term()/dynamic() by parametric types
{:erlang, :++, [{[list(term()), term()], dynamic(list(term(), term()))}]},
{:erlang, :--, [{[list(term()), list(term())], dynamic(list(term()))}]},
{:erlang, :andalso, [{[boolean(), term()], dynamic()}]},
{:erlang, :delete_element, [{[integer(), open_tuple([])], dynamic(open_tuple([]))}]},
{:erlang, :hd, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :element, [{[integer(), open_tuple([])], dynamic()}]},
{:erlang, :insert_element,
[{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
{:erlang, :max, [{[term(), term()], dynamic()}]},
{:erlang, :min, [{[term(), term()], dynamic()}]},
{:erlang, :orelse, [{[boolean(), term()], dynamic()}]},
{:erlang, :send, [{[send_destination, term()], dynamic()}]},
{:erlang, :setelement, [{[integer(), open_tuple([]), term()], dynamic(open_tuple([]))}]},
{:erlang, :tl, [{[non_empty_list(term(), term())], dynamic()}]},
{:erlang, :tuple_to_list, [{[open_tuple([])], dynamic(list(term()))}]}
] do
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
true =
Code.ensure_loaded?(mod) and
(function_exported?(mod, fun, arity) or fun in [:orelse, :andalso])
domain_clauses =
case clauses do
[_] ->
{:strong, nil, clauses}
_ ->
domain =
clauses
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
{:strong, domain, clauses}
end
defp remote(unquote(mod), unquote(fun), unquote(arity)),
do: unquote(Macro.escape(domain_clauses))
end
defp remote(_mod, _fun, _arity), do: :none
@doc """
Checks a module is a valid remote.
It returns either a tuple with the remote information and the context.
The remote information may be one of:
* `:none` - no typing information found.
* `{:infer, clauses}` - clauses from inferences. You must check all
all clauses and return the union between them. They are dynamic
and they can only be converted into arrows by computing the union
of all arguments.
* `{:strong, domain or nil, clauses}` - clauses from signatures. So far
these are strong arrows with non-overlapping domains
"""
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
if Keyword.get(meta, :runtime_module, false) do
{:none, context}
else
case remote(module, fun, arity) do
:none -> export(module, fun, arity, meta, stack, context)
clauses -> {clauses, context}
end
end
end
@doc """
Applies a function in unknown modules.
Used only by info functions.
"""
def apply(name, args_types, expr, stack, context) do
arity = length(args_types)
case remote(name, arity) do
:none -> {dynamic(), context}
info -> apply_remote(info, args_types, expr, stack, context)
end
end
@doc """
Applies a function in a given module.
"""
def apply(:erlang, :element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
when is_integer(index) do
case tuple_fetch(tuple, index - 1) do
{_optional?, value_type} ->
{value_type, context}
:badtuple ->
{error_type(), to_badapply_error(expr, [integer(), tuple], stack, context)}
reason ->
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
end
end
def apply(
:erlang,
:insert_element,
[_, tuple, value],
{_, meta, [index, _, _]} = expr,
stack,
context
)
when is_integer(index) do
case tuple_insert_at(tuple, index - 1, value) do
value_type when is_descr(value_type) ->
{value_type, context}
:badtuple ->
{error_type(), to_badapply_error(expr, [integer(), tuple, value], stack, context)}
reason ->
{error_type(), error({reason, expr, tuple, index - 2, context}, meta, stack, context)}
end
end
def apply(:erlang, :delete_element, [_, tuple], {_, meta, [index, _]} = expr, stack, context)
when is_integer(index) do
case tuple_delete_at(tuple, index - 1) do
value_type when is_descr(value_type) ->
{value_type, context}
:badtuple ->
{error_type(), to_badapply_error(expr, [integer(), tuple], stack, context)}
reason ->
{error_type(), error({reason, expr, tuple, index - 1, context}, meta, stack, context)}
end
end
def apply(:erlang, :make_tuple, [_, elem], {_, _meta, [size, _]}, _stack, context)
when is_integer(size) and size >= 0 do
{tuple(List.duplicate(elem, size)), context}
end
def apply(:erlang, :hd, [list], expr, stack, context) do
case list_hd(list) do
{_, value_type} ->
{value_type, context}
:badnonemptylist ->
{error_type(), to_badapply_error(expr, [list], stack, context)}
end
end
def apply(:erlang, :tl, [list], expr, stack, context) do
case list_tl(list) do
{_, value_type} ->
{value_type, context}
:badnonemptylist ->
{error_type(), to_badapply_error(expr, [list], stack, context)}
end
end
def apply(:erlang, name, [left, right] = args_types, expr, stack, context)
when name in [:>=, :"=<", :>, :<, :min, :max] do
context =
cond do
stack.mode == :infer ->
context
match?({false, _}, map_fetch(left, :__struct__)) or
match?({false, _}, map_fetch(right, :__struct__)) ->
warning = {:struct_comparison, expr, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
number_type?(left) and number_type?(right) ->
context
disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
true ->
context
end
if name in [:min, :max] do
{union(left, right), context}
else
{remote_return(boolean(), args_types, stack), context}
end
end
def apply(:erlang, name, [left, right] = args_types, expr, stack, context)
when name in [:==, :"/=", :"=:=", :"=/="] do
context =
cond do
stack.mode == :infer ->
context
name in [:==, :"/="] and number_type?(left) and number_type?(right) ->
context
disjoint?(left, right) ->
warning = {:mismatched_comparison, expr, context}
warn(__MODULE__, warning, elem(expr, 1), stack, context)
true ->
context
end
{remote_return(boolean(), args_types, stack), context}
end
def apply(mod, name, args_types, expr, stack, context) do
arity = length(args_types)
case :elixir_rewrite.inline(mod, name, arity) do
{mod, name} ->
apply(mod, name, args_types, expr, stack, context)
false ->
{info, context} = remote(mod, name, arity, elem(expr, 1), stack, context)
apply_remote(info, args_types, expr, stack, context)
end
end
defp remote_return(type, args_types, stack) do
cond do
stack.mode == :static -> type
Enum.any?(args_types, &gradual?/1) -> dynamic(type)
true -> type
end
end
defp apply_remote(info, args_types, expr, stack, context) do
case apply_remote(info, args_types, stack) do
{:ok, type} ->
{type, context}
{:error, domain, clauses} ->
error = {:badapply, expr, args_types, domain, clauses, context}
{error_type(), error(error, elem(expr, 1), stack, context)}
end
end
defp apply_remote(:none, _args_types, _stack) do
{:ok, dynamic()}
end
defp apply_remote({:strong, nil, [{expected, return}] = clauses}, args_types, stack) do
# Optimize single clauses as the domain is the single clause args.
case zip_compatible?(args_types, expected) do
true -> {:ok, remote_return(return, args_types, stack)}
false -> {:error, expected, clauses}
end
end
defp apply_remote({:strong, domain, clauses}, args_types, stack) do
# If the type is only gradual, the compatibility check is the same
# as a non disjoint check. So we skip checking compatibility twice.
with true <- zip_compatible_or_only_gradual?(args_types, domain),
[_ | _] = returns <-
for({expected, return} <- clauses, zip_not_disjoint?(args_types, expected), do: return) do
{:ok, returns |> Enum.reduce(&union/2) |> remote_return(args_types, stack)}
else
_ -> {:error, domain, clauses}
end
end
defp apply_remote({:infer, clauses}, args_types, _stack) do
case for({expected, return} <- clauses, zip_not_disjoint?(args_types, expected), do: return) do
[] ->
domain =
clauses
|> Enum.map(fn {args, _} -> args end)
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
{:error, domain, clauses}
returns ->
{:ok, returns |> Enum.reduce(&union/2) |> dynamic()}
end
end
defp zip_compatible_or_only_gradual?([actual | actuals], [expected | expecteds]) do
(only_gradual?(actual) or compatible?(actual, expected)) and
zip_compatible_or_only_gradual?(actuals, expecteds)
end
defp zip_compatible_or_only_gradual?([], []), do: true
defp zip_compatible?([actual | actuals], [expected | expecteds]) do
compatible?(actual, expected) and zip_compatible?(actuals, expecteds)
end
defp zip_compatible?([], []), do: true
defp zip_not_disjoint?([actual | actuals], [expected | expecteds]) do
not disjoint?(actual, expected) and zip_not_disjoint?(actuals, expecteds)
end
defp zip_not_disjoint?([], []), do: true
defp export(_module, :module_info, arity, _meta, _stack, context) when arity in [0, 1] do
{remote(:module_info, arity), context}
end
defp export(_module, _fun, _arity, _meta, %{cache: %Macro.Env{}}, context) do
{:none, context}
end
defp export(module, fun, arity, meta, stack, context) do
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
{:ok, mode, reason, info} ->
info = if info == :none, do: remote(fun, arity), else: info
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
{:error, type} ->
context =
if warn_undefined?(module, fun, arity, stack) do
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
else
context
end
{:none, context}
end
end
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
if reason do
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
reason = string |> List.to_string() |> :string.titlecase()
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
reason = string |> List.to_string() |> :string.titlecase()
reason = "It will be removed in #{removal}. #{reason}"
warn(__MODULE__, {:deprecated, module, fun, arity, reason}, meta, stack, context)
_ ->
context
end
end
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, stack) do
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
## Warning helpers
@doc """
@@ -987,6 +435,30 @@ defmodule Module.Types.Of do
}
end
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints(hints)
])
}
end
def format_diagnostic({:badmap, expr, type, key, context}) do
traces = collect_traces(expr, context)
@@ -1035,279 +507,11 @@ defmodule Module.Types.Of do
}
end
def format_diagnostic({:badindex, expr, type, index, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{format_mfa(expr)}:
#{expr_to_string(expr) |> indent(4)}
the given type does not have the given index:
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces)
])
}
end
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints(hints)
])
}
end
def format_diagnostic({:badapply, expr, args_types, domain, clauses, context}) do
traces = collect_traces(expr, context)
{{:., _, [mod, fun]}, _, args} = expr
{mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args)
explanation =
if i = Enum.find_index(args_types, &empty?/1) do
"""
the #{integer_to_ordinal(i + 1)} argument is empty (often represented as none()), \
most likely because it is the result of an expression that always fails, such as \
a `raise` or a previous invalid call. This causes any function called with this \
value to fail
"""
else
"""
but expected one of:
#{clauses_args_to_quoted_string(clauses, converter)}
"""
end
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types given to #{Exception.format_mfa(mod, fun, length(args))}:
#{expr_to_string(expr) |> indent(4)}
given types:
#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}
""",
explanation,
format_traces(traces)
])
}
end
def format_diagnostic({:mismatched_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison between distinct types found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
While Elixir can compare across all types, you are comparing \
across types which are always disjoint, and the result is either \
always true or always false
"""
])
}
end
def format_diagnostic({:struct_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Structs that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison.
"""
])
}
end
def format_diagnostic({:undefined, :module, module, fun, arity}) do
top =
if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}"
else
Exception.format_mfa(module, fun, arity)
end
%{
message:
IO.iodata_to_binary([
top,
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)",
UndefinedFunctionError.hint_for_missing_module(module, fun, arity)
]),
group: true
}
end
def format_diagnostic({:undefined, :function, module, :__struct__, 0}) do
%{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)",
group: true
}
end
def format_diagnostic({:undefined, :function, module, fun, arity}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity)
]),
group: true
}
end
def format_diagnostic({:deprecated, module, fun, arity, reason}) do
%{
message:
IO.iodata_to_binary([
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]),
group: true
}
end
defp pluralize(1, singular, _), do: "1 #{singular}"
defp pluralize(i, _, plural), do: "#{i} #{plural}"
defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
defp to_badapply_error({{:., _, [mod, fun]}, meta, _} = expr, args_types, stack, context) do
{_type, domain, [{args, _} | _] = clauses} = remote(mod, fun, length(args_types))
error({:badapply, expr, args_types, domain || args, clauses, context}, meta, stack, context)
end
defp empty_if(condition, content) do
if condition, do: "", else: content
end
defp format_mfa({{:., _, [mod, fun]}, _, args}) do
{mod, fun, args, _} = :elixir_rewrite.erl_to_ex(mod, fun, args)
Exception.format_mfa(mod, fun, length(args))
end
## Algebra helpers
alias Inspect.Algebra, as: IA
defp clauses_args_to_quoted_string([{args, _return}], converter) do
"\n " <> (clause_args_to_quoted_string(args, converter) |> indent(4))
end
defp clauses_args_to_quoted_string(clauses, converter) do
clauses
|> Enum.with_index(fn {args, _return}, index ->
"""
##{index + 1}
#{clause_args_to_quoted_string(args, converter)}\
"""
|> indent(4)
end)
|> Enum.join("\n")
end
defp clause_args_to_quoted_string(args, converter) do
docs = Enum.map(args, &(&1 |> to_quoted() |> Code.Formatter.to_algebra()))
args_docs_to_quoted_string(converter.(docs))
end
defp args_to_quoted_string(args_types, domain, converter) do
ansi? = IO.ANSI.enabled?()
docs =
Enum.zip_with(args_types, domain, fn actual, expected ->
doc = actual |> to_quoted() |> Code.Formatter.to_algebra()
cond do
compatible?(actual, expected) -> doc
ansi? -> IA.concat(IA.color(doc, IO.ANSI.red()), IA.color(IA.empty(), IO.ANSI.reset()))
true -> IA.concat(["-", doc, "-"])
end
end)
args_docs_to_quoted_string(converter.(docs))
end
defp args_docs_to_quoted_string(docs) do
doc = IA.fold(docs, fn doc, acc -> IA.glue(IA.concat(doc, ","), acc) end)
wrapped_docs =
case docs do
[_] -> IA.concat("(", IA.concat(doc, ")"))
_ -> IA.group(IA.glue(IA.nest(IA.glue("(", "", doc), 2), "", ")"))
end
wrapped_docs
|> IA.format(98)
|> IO.iodata_to_binary()
|> case do
"(\n" <> _ = multiple_lines -> multiple_lines
single_line -> binary_slice(single_line, 1..-2//1)
end
end
defp integer_to_ordinal(i) do
case rem(i, 10) do
1 when rem(i, 100) != 11 -> "#{i}st"
2 when rem(i, 100) != 12 -> "#{i}nd"
3 when rem(i, 100) != 13 -> "#{i}rd"
_ -> "#{i}th"
end
end
end
+50 -48
View File
@@ -25,6 +25,10 @@ defmodule Module.Types.Pattern do
is refined, we restart at step 2.
"""
def of_head(_patterns, _guards, expected, _tag, _meta, %{mode: :traversal}, context) do
{expected, context}
end
def of_head(patterns, guards, expected, tag, meta, stack, context) do
stack = %{stack | meta: meta}
@@ -98,7 +102,13 @@ defmodule Module.Types.Pattern do
This version tracks the whole expression in tracing,
instead of only the pattern.
"""
def of_match(pattern, guards \\ [], expected, expr, tag, stack, context) do
def of_match(pattern, guards \\ [], expected, expr, tag, stack, context)
def of_match(_pattern, _guards, expected, _expr, _tag, %{mode: :traversal}, context) do
{expected, context}
end
def of_match(pattern, guards, expected, expr, tag, stack, context) do
context = init_pattern_info(context)
{tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context)
@@ -114,32 +124,39 @@ defmodule Module.Types.Pattern do
{type, context}
end
defp all_single_path?(vars, info, index) do
info
|> Map.get(index, [])
|> Enum.all?(fn version -> match?([_], Map.fetch!(vars, version)) end)
end
defp of_pattern_recur(types, tag, stack, context, callback) do
%{pattern_info: {pattern_vars, pattern_info, _counter}} = context
%{pattern_info: {vars, info, _counter}} = context
context = nilify_pattern_info(context)
pattern_vars = Map.to_list(pattern_vars)
changed = :lists.seq(0, length(types) - 1)
# If all variables in a given index have a single path,
# then there are no changes to propagate
unchangeable = for index <- changed, all_single_path?(vars, info, index), do: index
vars = Map.to_list(vars)
try do
case callback.(types, changed, context) do
{:ok, types, context} ->
of_pattern_recur(types, pattern_vars, pattern_info, tag, stack, context, callback)
of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback)
{:error, context} ->
{types, error_vars(pattern_vars, context)}
{types, error_vars(vars, context)}
end
catch
{types, context} -> {types, error_vars(pattern_vars, context)}
{types, context} -> {types, error_vars(vars, context)}
end
end
defp of_pattern_recur(types, vars, info, tag, stack, context, callback) do
%{vars: context_vars} = context
defp of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback) do
{changed, context} =
Enum.reduce(vars, {[], context}, fn {version, paths}, {changed, context} ->
current_type = context_vars[version][:type]
{var_changed?, context} =
Enum.reduce(paths, {false, context}, fn
[var, {:arg, index, expr} | path], {var_changed?, context} ->
@@ -147,18 +164,20 @@ defmodule Module.Types.Pattern do
case of_pattern_var(path, actual, true, info, context) do
{type, reachable_var?} ->
case Of.refine_var(var, type, expr, stack, context) do
{:ok, type, context} ->
{var_changed? or
(reachable_var? and
(current_type == nil or not equal?(current_type, type))), context}
{:error, _type, context} ->
throw({types, context})
# If current type is already a subtype, there is nothing to refine.
with %{^version => %{type: current_type}} <- context.vars,
true <- subtype?(current_type, type) do
{var_changed?, context}
else
_ ->
case Of.refine_var(var, type, expr, stack, context) do
{:ok, _type, context} -> {var_changed? or reachable_var?, context}
{:error, _type, context} -> throw({types, context})
end
end
:error ->
throw({types, to_badpattern_error(expr, tag, stack, context)})
throw({types, badpattern_error(expr, tag, stack, context)})
end
end)
@@ -167,23 +186,12 @@ defmodule Module.Types.Pattern do
{changed, context}
true ->
case paths do
# A single change, check if there are other variables in this index.
[[_var, {:arg, index, _} | _]] ->
case info do
%{^index => true} -> {[index | changed], context}
%{^index => false} -> {changed, context}
end
# Several changes, we have to recompute all indexes.
_ ->
var_changed = Enum.map(paths, fn [_var, {:arg, index, _} | _] -> index end)
{var_changed ++ changed, context}
end
var_changed = Enum.map(paths, fn [_var, {:arg, index, _} | _] -> index end)
{var_changed ++ changed, context}
end
end)
case :lists.usort(changed) do
case :lists.usort(changed) -- unchangeable do
[] ->
{types, context}
@@ -194,7 +202,7 @@ defmodule Module.Types.Pattern do
{types, context}
{:ok, types, context} ->
of_pattern_recur(types, vars, info, tag, stack, context, callback)
of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback)
{:error, context} ->
{types, error_vars(vars, context)}
@@ -208,7 +216,7 @@ defmodule Module.Types.Pattern do
end)
end
defp to_badpattern_error(expr, tag, stack, context) do
defp badpattern_error(expr, tag, stack, context) do
meta =
if meta = get_meta(expr) do
meta ++ Keyword.take(stack.meta, [:generated, :line])
@@ -224,7 +232,7 @@ defmodule Module.Types.Pattern do
type = intersection(actual, expected)
if empty?(type) do
{:error, to_badpattern_error(expr, tag, stack, context)}
{:error, badpattern_error(expr, tag, stack, context)}
else
{:ok, type, context}
end
@@ -251,8 +259,8 @@ defmodule Module.Types.Pattern do
end
# TODO: Implement domain key types
defp of_pattern_var([{:key, _key} | rest], _type, reachable_var?, info, context) do
of_pattern_var(rest, dynamic(), reachable_var?, info, context)
defp of_pattern_var([{:key, _key} | rest], _type, _reachable_var?, info, context) do
of_pattern_var(rest, dynamic(), false, info, context)
end
defp of_pattern_var([{:head, counter} | rest], type, _reachable_var?, info, context) do
@@ -489,14 +497,8 @@ defmodule Module.Types.Pattern do
paths = [[var | path] | Map.get(vars, version, [])]
vars = Map.put(vars, version, paths)
# Our goal here is to compute if an argument has more than one variable.
info =
case info do
%{^arg => false} -> %{info | arg => true}
%{^arg => true} -> info
%{} -> Map.put(info, arg, false)
end
# Stores all variables used at any given argument
info = Map.update(info, arg, [version], &[version | &1])
{{:var, version}, %{context | pattern_info: {vars, info, counter}}}
end
@@ -715,7 +717,7 @@ defmodule Module.Types.Pattern do
{args_type, context} =
Enum.map_reduce(args, context, &of_guard(&1, dynamic(), expr, stack, &2))
Of.apply(:erlang, function, args_type, call, stack, context)
Module.Types.Apply.remote(:erlang, function, args_type, call, stack, context)
end
# var
+1 -1
View File
@@ -90,7 +90,7 @@ defmodule Set do
if target1 == target2 do
target1.intersection(set1, set2)
else
Enumerable.reduce(set1, {:cont, target1.new}, fn v, acc ->
Enumerable.reduce(set1, {:cont, target1.new()}, fn v, acc ->
{:cont, if(target2.member?(set2, v), do: target1.put(acc, v), else: acc)}
end)
|> elem(1)
+1 -1
View File
@@ -88,9 +88,9 @@ start(_Type, _Args) ->
%% Compiler options
{docs, true},
{infer_signatures, true},
{ignore_already_consolidated, false},
{ignore_module_conflict, false},
{infer_signatures, true},
{on_undefined_variable, raise},
{parser_options, [{columns, true}]},
{debug_info, true},
+1 -1
View File
@@ -50,7 +50,7 @@ define({Line, _S, #{module := Module} = E}, Kind, Call, Expr) ->
Key
end,
Args = [Kind, Store, elixir_locals:cache_env(E#{line := Line})],
Args = [Kind, Store, elixir_module:cache_env(E#{line := Line})],
{{'.', [], [elixir_def, store_definition]}, [], Args}.
unless_loaded(Fun, Args, Callback) ->
+1 -2
View File
@@ -144,7 +144,6 @@ bootstrap() ->
elixir_config:static(#{bootstrap => true}),
elixir_config:put(docs, false),
elixir_config:put(ignore_module_conflict, true),
elixir_config:put(infer_signatures, false),
elixir_config:put(on_undefined_variable, raise),
elixir_config:put(parser_options, []),
elixir_config:put(relative_paths, false),
@@ -191,13 +190,13 @@ bootstrap_files() ->
[
<<"list/chars.ex">>,
<<"bitwise.ex">>,
<<"module/locals_tracker.ex">>,
<<"module/parallel_checker.ex">>,
<<"module/behaviour.ex">>,
<<"module/types/helpers.ex">>,
<<"module/types/descr.ex">>,
<<"module/types/of.ex">>,
<<"module/types/pattern.ex">>,
<<"module/types/apply.ex">>,
<<"module/types/expr.ex">>,
<<"module/types.ex">>,
<<"exception.ex">>,
+7 -3
View File
@@ -37,6 +37,7 @@ fun_for(Meta, Module, Name, Arity, Kinds, External) ->
{[{_, Kind, LocalMeta, _, _, _}], ClausesPairs} ->
case (Kinds == all) orelse (lists:member(Kind, Kinds)) of
true ->
(Kind == defmacrop) andalso track_defmacrop(Module, Tuple),
Local = {value, fun(Fun, Args) -> invoke_local(Meta, Module, Fun, Args, External) end},
Clauses = [Clause || {_, Clause} <- ClausesPairs],
elixir_erl:definition_to_anonymous(Kind, LocalMeta, Clauses, Local, External);
@@ -60,6 +61,10 @@ invoke_local(Meta, Module, ErlName, Args, External) ->
apply(Fun, Args)
end.
track_defmacrop(Module, FunArity) ->
{_, Bag} = elixir_module:data_tables(Module),
ets:insert(Bag, {defmacrop_calls, FunArity}).
invoke_external(Meta, Mod, Name, Args, E) ->
is_map(E) andalso elixir_env:trace({require, Meta, Mod, []}, E),
apply(Mod, Name, Args).
@@ -128,10 +133,10 @@ head_and_definition_meta(_, _Meta, _HeadDefaults, [{_, _, HeadMeta, _} | _]) ->
%% Section for storing definitions
store_definition(Kind, {Call, Body}, Pos) ->
E = elixir_locals:get_cached_env(Pos),
E = elixir_module:get_cached_env(Pos),
store_definition(Kind, false, Call, Body, E);
store_definition(Kind, Key, Pos) ->
#{module := Module} = E = elixir_locals:get_cached_env(Pos),
#{module := Module} = E = elixir_module:get_cached_env(Pos),
{Call, Body} = elixir_module:read_cache(Module, Key),
store_definition(Kind, true, Call, Body, E).
@@ -210,7 +215,6 @@ store_definition(Meta, Kind, CheckClauses, Name, Arity, DefaultsArgs, Guards, Bo
Clause <- def_to_clauses(Kind, Meta, Args, Guards, Body, E)],
DefaultsLength = length(Defaults),
elixir_locals:record_defaults(Tuple, Kind, Module, DefaultsLength, Meta),
check_previous_defaults(Meta, Module, Name, Arity, Kind, DefaultsLength, E),
store_definition(CheckClauses, Kind, Meta, Name, Arity, File,
+4 -8
View File
@@ -55,7 +55,7 @@ import_function(Meta, Name, Arity, E) ->
case find_import_by_name_arity(Meta, Tuple, [], E) of
{function, Receiver} ->
elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E),
elixir_locals:record_import(Tuple, Receiver, ?key(E, module), ?key(E, function)),
elixir_import:record(Tuple, Receiver, ?key(E, module), ?key(E, function)),
remote_function(Meta, Receiver, Name, Arity, E);
{macro, _Receiver} ->
false;
@@ -74,7 +74,6 @@ import_function(Meta, Name, Arity, E) ->
elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E) of
false ->
elixir_env:trace({local_function, Meta, Name, Arity}, E),
elixir_locals:record_local(Tuple, ?key(E, module), ?key(E, function), Meta, false),
{local, Name, Arity};
_ ->
false
@@ -183,10 +182,7 @@ expand_import(Meta, Name, Arity, E, Extra, AllowLocals, Trace) ->
%% Dispatch to the local.
_ ->
Trace andalso begin
elixir_env:trace({local_macro, Meta, Name, Arity}, E),
elixir_locals:record_local(Tuple, Module, ?key(E, function), Meta, true)
end,
Trace andalso elixir_env:trace({local_macro, Meta, Name, Arity}, E),
{macro, Module, expander_macro_fun(Meta, Local, Module, Name, E)}
end
end.
@@ -196,13 +192,13 @@ do_expand_import(Result, Meta, Name, Arity, Module, E, Trace) ->
{function, Receiver} ->
Trace andalso begin
elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E),
elixir_locals:record_import({Name, Arity}, Receiver, Module, ?key(E, function))
elixir_import:record({Name, Arity}, Receiver, Module, ?key(E, function))
end,
{function, Receiver, Name};
{macro, Receiver} ->
Trace andalso begin
elixir_env:trace({imported_macro, Meta, Receiver, Name, Arity}, E),
elixir_locals:record_import({Name, Arity}, Receiver, Module, ?key(E, function))
elixir_import:record({Name, Arity}, Receiver, Module, ?key(E, function))
end,
{macro, Receiver, expander_macro_named(Meta, Receiver, Name, Arity, E)};
{import, Receiver} ->
+1 -2
View File
@@ -900,8 +900,7 @@ expand_local(Meta, Name, Args, S, #{module := Module, function := Function, cont
nil ->
Arity = length(Args),
elixir_env:trace({local_function, Meta, Name, Arity}, E),
elixir_locals:record_local({Name, Arity}, Module, Function, Meta, false)
elixir_env:trace({local_function, Meta, Name, Arity}, E)
end,
{EArgs, SA, EA} = expand_args(Args, S, E),
+33 -2
View File
@@ -2,7 +2,9 @@
%% between local functions and imports.
%% For imports dispatch, please check elixir_dispatch.
-module(elixir_import).
-export([import/6, import/7, special_form/2, format_error/1]).
-export([import/6, import/7, special_form/2,
record/4, ensure_no_local_conflict/3,
format_error/1]).
-compile(inline_list_funcs).
-include("elixir.hrl").
@@ -135,6 +137,31 @@ calculate_key(Meta, Key, Old, New, E, Warn) ->
{true, FinalSet, [{Key, FinalSet} | keydelete(Key, Old)]}
end.
%% Record function calls for local conflicts
record(_Tuple, Receiver, Module, Function)
when Function == nil; Module == Receiver -> false;
record(Tuple, Receiver, Module, _Function) ->
try
{Set, _Bag} = elixir_module:data_tables(Module),
ets:insert(Set, {{import, Tuple}, Receiver}),
true
catch
error:badarg -> false
end.
ensure_no_local_conflict('Elixir.Kernel', _All, _E) ->
ok;
ensure_no_local_conflict(Module, AllDefinitions, E) ->
{Set, _} = elixir_module:data_tables(Module),
[try
Receiver = ets:lookup_element(Set, {import, Pair}, 2),
elixir_errors:module_error(Meta, E, ?MODULE, {import_conflict, Receiver, Pair})
catch
error:badarg -> false
end || {Pair, _, Meta, _} <- AllDefinitions].
%% Retrieve functions and macros from modules
get_functions(Module, InfoCallback) ->
@@ -229,7 +256,11 @@ format_error({special_form_conflict, {Receiver, Name, Arity}}) ->
[elixir_aliases:inspect(Receiver), Name, Arity]);
format_error({no_macros, Module}) ->
io_lib:format("could not load macros from module ~ts", [elixir_aliases:inspect(Module)]).
io_lib:format("could not load macros from module ~ts", [elixir_aliases:inspect(Module)]);
format_error({import_conflict, Receiver, {Name, Arity}}) ->
io_lib:format("imported ~ts.~ts/~B conflicts with local function",
[elixir_aliases:inspect(Receiver), Name, Arity]).
%% LIST HELPERS
-146
View File
@@ -1,146 +0,0 @@
%% Module responsible for tracking invocations of module calls.
-module(elixir_locals).
-export([
setup/1, stop/1, cache_env/1, get_cached_env/1,
record_local/5, record_import/4, record_defaults/5,
yank/2, reattach/6, ensure_no_import_conflict/3,
warn_unused_local/4, ensure_no_undefined_local/3,
format_error/1
]).
-include("elixir.hrl").
-define(cache_key, {elixir, cache_env}).
-define(locals_key, {elixir, locals}).
-define(locals, 'Elixir.Module.LocalsTracker').
setup({DataSet, _DataBag}) ->
ets:insert(DataSet, {?cache_key, 0}),
case elixir_config:is_bootstrap() of
false -> ets:insert(DataSet, {?locals_key, true});
true -> ok
end,
ok.
stop({DataSet, _DataBag}) ->
ets:delete(DataSet, ?locals_key).
yank(Tuple, Module) ->
if_tracker(Module, fun(Tracker) -> ?locals:yank(Tracker, Tuple) end).
reattach(Tuple, Kind, Module, Function, Neighbours, Meta) ->
if_tracker(Module, fun(Tracker) -> ?locals:reattach(Tracker, Tuple, Kind, Function, Neighbours, Meta) end).
record_local(_Tuple, _Module, nil, _Meta, _IsMacroDispatch) ->
ok;
record_local(Tuple, Module, Function, Meta, IsMacroDispatch) ->
if_tracker(Module, fun(Tracker) -> ?locals:add_local(Tracker, Function, Tuple, Meta, IsMacroDispatch), ok end).
record_import(_Tuple, Receiver, Module, Function)
when Function == nil; Module == Receiver -> false;
record_import(Tuple, Receiver, Module, Function) ->
if_tracker(Module, fun(Tracker) -> ?locals:add_import(Tracker, Function, Receiver, Tuple), ok end).
record_defaults(_Tuple, _Kind, _Module, 0, _Meta) ->
ok;
record_defaults(Tuple, Kind, Module, Defaults, Meta) ->
if_tracker(Module, fun(Tracker) -> ?locals:add_defaults(Tracker, Kind, Tuple, Defaults, Meta), ok end).
if_tracker(Module, Callback) ->
if_tracker(Module, ok, Callback).
if_tracker(Module, Default, Callback) ->
try
{DataSet, _} = Tables = elixir_module:data_tables(Module),
{ets:member(DataSet, ?locals_key), Tables}
of
{true, Tracker} -> Callback(Tracker);
{false, _} -> Default
catch
error:badarg -> Default
end.
%% CACHING
cache_env(#{line := Line, module := Module} = E) ->
{Set, _} = elixir_module:data_tables(Module),
Cache = elixir_env:reset_vars(E#{line := nil}),
PrevKey = ets:lookup_element(Set, ?cache_key, 2),
Pos =
case ets:lookup(Set, {cache_env, PrevKey}) of
[{_, Cache}] ->
PrevKey;
_ ->
NewKey = PrevKey + 1,
ets:insert(Set, [{{cache_env, NewKey}, Cache}, {?cache_key, NewKey}]),
NewKey
end,
{Module, {Line, Pos}}.
get_cached_env({Module, {Line, Pos}}) ->
{Set, _} = elixir_module:data_tables(Module),
(ets:lookup_element(Set, {cache_env, Pos}, 2))#{line := Line};
get_cached_env(Env) ->
Env.
%% ERROR HANDLING
ensure_no_import_conflict('Elixir.Kernel', _All, _E) ->
ok;
ensure_no_import_conflict(Module, All, E) ->
if_tracker(Module, ok, fun(Tracker) ->
[elixir_errors:module_error(Meta, E, ?MODULE, {function_conflict, Error})
|| {Meta, Error} <- ?locals:collect_imports_conflicts(Tracker, All)],
ok
end).
ensure_no_undefined_local(Module, All, E) ->
if_tracker(Module, [], fun(Tracker) ->
[elixir_errors:module_error(Meta, E#{function := Function, file := File}, ?MODULE, {Error, Tuple, Module})
|| {Function, Meta, File, Tuple, Error} <- ?locals:collect_undefined_locals(Tracker, All, ?key(E, file))],
ok
end).
warn_unused_local(Module, All, Private, E) ->
if_tracker(Module, [], fun(Tracker) ->
{Unreachable, Warnings} = ?locals:collect_unused_locals(Tracker, All, Private),
[elixir_errors:file_warn(Meta, E, ?MODULE, Error) || {Meta, Error} <- Warnings],
Unreachable
end).
format_error({function_conflict, {Receiver, {Name, Arity}}}) ->
io_lib:format("imported ~ts.~ts/~B conflicts with local function",
[elixir_aliases:inspect(Receiver), Name, Arity]);
format_error({unused_args, {Name, Arity}}) ->
io_lib:format("default values for the optional arguments in ~ts/~B are never used", [Name, Arity]);
format_error({unused_args, {Name, Arity}, Count}) when Arity - Count == 1 ->
io_lib:format("the default value for the last optional argument in ~ts/~B is never used", [Name, Arity]);
format_error({unused_args, {Name, Arity}, Count}) ->
io_lib:format("the default values for the last ~B optional arguments in ~ts/~B are never used", [Arity - Count, Name, Arity]);
format_error({unused_def, {Name, Arity}, defp}) ->
io_lib:format("function ~ts/~B is unused", [Name, Arity]);
format_error({unused_def, {Name, Arity}, defmacrop}) ->
io_lib:format("macro ~ts/~B is unused", [Name, Arity]);
format_error({undefined_function, {F, A}, _})
when F == '__info__', A == 1;
F == 'behaviour_info', A == 1;
F == 'module_info', A == 1;
F == 'module_info', A == 0 ->
io_lib:format("undefined function ~ts/~B (this function is auto-generated by the compiler "
"and must always be called as a remote, as in __MODULE__.~ts/~B)", [F, A, F, A]);
format_error({undefined_function, {F, A}, Module}) ->
io_lib:format("undefined function ~ts/~B (expected ~ts to define such a function or "
"for it to be imported, but none are available)", [F, A, elixir_aliases:inspect(Module)]);
format_error({incorrect_dispatch, {F, A}, _Module}) ->
io_lib:format("cannot invoke macro ~ts/~B before its definition", [F, A]).
+48 -27
View File
@@ -1,9 +1,10 @@
-module(elixir_module).
-export([file/1, data_tables/1, is_open/1, mode/1, delete_definition_attributes/6,
compile/6, expand_callback/6, format_error/1, compiler_modules/0,
write_cache/3, read_cache/2, next_counter/1, taint/1]).
write_cache/3, read_cache/2, next_counter/1, taint/1, cache_env/1, get_cached_env/1]).
-include("elixir.hrl").
-define(counter_attr, {elixir, counter}).
-define(cache_key, {elixir, cache_env}).
%% Stores modules currently being defined by the compiler
@@ -71,6 +72,29 @@ taint(Module) ->
_:_ -> false
end.
cache_env(#{line := Line, module := Module} = E) ->
{Set, _} = data_tables(Module),
Cache = elixir_env:reset_vars(E#{line := nil}),
PrevKey = ets:lookup_element(Set, ?cache_key, 2),
Pos =
case ets:lookup(Set, {cache_env, PrevKey}) of
[{_, Cache}] ->
PrevKey;
_ ->
NewKey = PrevKey + 1,
ets:insert(Set, [{{cache_env, NewKey}, Cache}, {?cache_key, NewKey}]),
NewKey
end,
{Module, {Line, Pos}}.
get_cached_env({Module, {Line, Pos}}) ->
{Set, _} = data_tables(Module),
(ets:lookup_element(Set, {cache_env, Pos}, 2))#{line := Line};
get_cached_env(Env) ->
Env.
%% Compilation hook
compile(Meta, Module, Block, Vars, Prune, Env) ->
@@ -146,37 +170,30 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
NifsAttribute = lists:keyfind(nifs, 1, Attributes),
validate_nifs_attribute(NifsAttribute, AllDefinitions, Line, E),
Unreachable = elixir_locals:warn_unused_local(Module, AllDefinitions, NewPrivate, E),
elixir_locals:ensure_no_undefined_local(Module, AllDefinitions, E),
elixir_locals:ensure_no_import_conflict(Module, AllDefinitions, E),
%% We stop tracking locals here to avoid race conditions in case after_load
%% evaluates code in a separate process that may write to locals table.
elixir_locals:stop({DataSet, DataBag}),
elixir_import:ensure_no_local_conflict(Module, AllDefinitions, E),
make_readonly(Module),
(not elixir_config:is_bootstrap()) andalso
'Elixir.Module':'__check_attributes__'(E, DataSet, DataBag),
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, Line, E),
Impls = bag_lookup_element(DataBag, impls, 2),
AfterVerify = bag_lookup_element(DataBag, {accumulate, after_verify}, 2),
[elixir_env:trace({remote_function, [], VerifyMod, VerifyFun, 1}, CallbackE) ||
{VerifyMod, VerifyFun} <- AfterVerify],
%% Compute signatures only if the module is valid.
case ets:member(DataSet, {elixir, taint}) of
true -> elixir_errors:compile_error(E);
false -> ok
end,
%% Ensure there are no errors before we infer types
compile_error_if_tainted(DataSet, E),
Signatures = case elixir_config:get(infer_signatures) of
true -> 'Elixir.Module.Types':infer(Module, File, AllDefinitions, CallbackE);
false -> #{}
end,
{Signatures, Unreachable} =
case elixir_config:is_bootstrap() of
true -> {#{}, []};
false ->
Defmacrop = bag_lookup_element(DataBag, defmacrop_calls, 2),
'Elixir.Module.Types':infer(Module, File, AllDefinitions, NewPrivate, Defmacrop, E)
end,
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, Line, E),
Impls = bag_lookup_element(DataBag, impls, 2),
ModuleMap = #{
struct => get_struct(DataSet),
@@ -186,15 +203,16 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
relative_file => elixir_utils:relative_to_cwd(File),
attributes => Attributes,
definitions => AllDefinitions,
unreachable => Unreachable,
after_verify => AfterVerify,
compile_opts => CompileOpts,
deprecated => get_deprecated(DataBag),
defines_behaviour => defines_behaviour(DataBag),
impls => Impls,
unreachable => Unreachable,
signatures => Signatures
},
compile_error_if_tainted(DataSet, E),
Binary = elixir_erl:compile(ModuleMap),
Autoload = proplists:get_value(autoload, CompileOpts, true),
spawn_parallel_checker(CheckerInfo, Module, ModuleMap),
@@ -228,6 +246,12 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
elixir_code_server:call({undefmodule, Ref})
end.
compile_error_if_tainted(DataSet, E) ->
case ets:member(DataSet, {elixir, taint}) of
true -> elixir_errors:compile_error(E);
false -> ok
end.
validate_compile_opts(Opts, Defs, Unreachable, Line, E) ->
lists:flatmap(fun (Opt) -> validate_compile_opt(Opt, Defs, Unreachable, Line, E) end, Opts).
@@ -351,7 +375,6 @@ build(Module, Line, File, E) ->
%% * {{type, Tuple}, ...}, {{opaque, Tuple}, ...}
%% * {{callback, Tuple}, ...}, {{macrocallback, Tuple}, ...}
%% * {{def, Tuple}, ...} (from elixir_def)
%% * {{import, Tuple}, ...} (from elixir_locals)
%% * {{overridable, Tuple}, ...} (from elixir_overridable)
%%
DataSet = ets:new(Module, [set, public]),
@@ -367,8 +390,6 @@ build(Module, Line, File, E) ->
%% * {overridables, ...} (from elixir_overridable)
%% * {{default, Name}, ...} (from elixir_def)
%% * {{clauses, Tuple}, ...} (from elixir_def)
%% * {reattach, ...} (from elixir_locals)
%% * {{local, Tuple}, ...} (from elixir_locals)
%%
DataBag = ets:new(Module, [duplicate_bag, public]),
@@ -395,6 +416,7 @@ build(Module, Line, File, E) ->
{optional_callbacks, [], accumulate, []},
% Others
{?cache_key, 0},
{?counter_attr, 0}
]),
@@ -411,7 +433,6 @@ build(Module, Line, File, E) ->
%% Setup definition related modules
Tables = {DataSet, DataBag},
elixir_def:setup(Tables),
elixir_locals:setup(Tables),
Tuple = {Module, Tables, Line, File, all},
Ref =
+8 -9
View File
@@ -1,10 +1,10 @@
% Holds the logic responsible for defining overridable functions and handling super.
-module(elixir_overridable).
-export([overridables_for/1, overridable_for/2,
record_overridable/4, super/4,
record_overridable/3, super/4,
store_not_overridden/1, format_error/1]).
-include("elixir.hrl").
-define(overridden_pos, 5).
-define(overridden_pos, 4).
overridables_for(Module) ->
{_, Bag} = elixir_module:data_tables(Module),
@@ -22,20 +22,20 @@ overridable_for(Module, Tuple) ->
[] -> not_overridable
end.
record_overridable(Module, Tuple, Def, Neighbours) ->
record_overridable(Module, Tuple, Def) ->
{Set, Bag} = elixir_module:data_tables(Module),
case ets:insert_new(Set, {{overridable, Tuple}, 1, Def, Neighbours, false}) of
case ets:insert_new(Set, {{overridable, Tuple}, 1, Def, false}) of
true ->
ets:insert(Bag, {overridables, Tuple});
false ->
[{_, Count, PreviousDef, _, _}] = ets:lookup(Set, {overridable, Tuple}),
[{_, Count, PreviousDef, _}] = ets:lookup(Set, {overridable, Tuple}),
{{_, Kind, Meta, File, _, _}, _} = Def,
{{_, PreviousKind, _, _, _, _}, _} = PreviousDef,
case is_valid_kind(Kind, PreviousKind) of
true ->
ets:insert(Set, {{overridable, Tuple}, Count + 1, Def, Neighbours, false});
ets:insert(Set, {{overridable, Tuple}, Count + 1, Def, false});
false ->
elixir_errors:file_error(Meta, File, ?MODULE, {bad_kind, Module, Tuple, Kind})
end
@@ -74,7 +74,7 @@ store_not_overridden(Module) ->
%% Private
store(Set, Module, Tuple, {_, Count, Def, Neighbours, Overridden}, Hidden) ->
store(Set, Module, Tuple, {_, Count, Def, Overridden}, Hidden) ->
{{{def, {Name, Arity}}, Kind, Meta, File, _Check,
{Defaults, _HasBody, _LastDefaults}}, Clauses} = Def,
@@ -92,8 +92,7 @@ store(Set, Module, Tuple, {_, Count, Def, Neighbours, Overridden}, Hidden) ->
false ->
ets:update_element(Set, {overridable, Tuple}, {?overridden_pos, true}),
elixir_def:store_definition(none, FinalKind, Meta, FinalName, FinalArity,
File, Module, Defaults, FinalClauses),
elixir_locals:reattach({FinalName, FinalArity}, FinalKind, Module, Tuple, Neighbours, Meta);
File, Module, Defaults, FinalClauses);
true ->
ok
end,
@@ -739,8 +739,8 @@ defmodule Kernel.ErrorsTest do
)
assert_compile_error(
["nofile:1: ", "undefined function foo/1 given to @compile :inline"],
~c"defmodule Test do @compile {:inline, foo: 1}; defmacro foo(_) end"
["nofile:1: ", "macro foo/1 given to @compile :inline"],
~c"defmodule Test do @compile {:inline, foo: 1}; defmacro foo(_), do: :ok end"
)
end
@@ -383,8 +383,6 @@ defmodule Kernel.QuoteTest.ErrorsTest do
mod = Kernel.QuoteTest.ErrorsTest
file = __ENV__.file |> Path.relative_to_cwd() |> String.to_charlist()
assert [{^mod, :will_raise, 2, [file: ^file, line: @line] ++ _} | _] = __STACKTRACE__
else
_ -> flunk("expected failure")
end
end
+2 -4
View File
@@ -458,12 +458,10 @@ defmodule Kernel.RaiseTest do
assert result == "no match of right hand side value: 0"
end
defp empty_map(), do: %{}
test "bad key error" do
result =
try do
%{empty_map() | foo: :bar}
%{Process.get(:unused, %{}) | foo: :bar}
rescue
x in [KeyError] -> Exception.message(x)
end
@@ -472,7 +470,7 @@ defmodule Kernel.RaiseTest do
result =
try do
empty_map().foo
Process.get(:unused, %{}).foo
rescue
x in [KeyError] -> Exception.message(x)
end
@@ -68,12 +68,10 @@ defmodule Kernel.SpecialFormsTest do
end
end
def false_fun(), do: false
test "cond_clause error keeps line number in stacktrace" do
try do
cond do
false_fun() -> :ok
Process.get(:unused, false) -> :ok
end
rescue
_ ->
+1 -1
View File
@@ -126,7 +126,7 @@ defmodule Kernel.WithTest do
end
assert_raise RuntimeError, fn ->
with({:ok, res} <- ok(42), res = res + oops(), do: res)
with({:ok, res} <- ok(42), oops(), do: res)
end
end
+3 -4
View File
@@ -11,7 +11,6 @@ defmodule KernelTest do
def id(arg), do: arg
def id(arg1, arg2), do: {arg1, arg2}
def empty_list(), do: []
def empty_map, do: %{}
defp purge(module) do
@@ -1359,7 +1358,7 @@ defmodule KernelTest do
assert is_map_key(Map.new(a: 1), :a) == true
assert_raise BadMapError, fn ->
is_map_key(empty_list(), :a)
is_map_key(Process.get(:unused, []), :a)
end
case Map.new(a: 1) do
@@ -1381,7 +1380,7 @@ defmodule KernelTest do
test "tl/1" do
assert tl([:one]) == []
assert tl([1, 2, 3]) == [2, 3]
assert_raise ArgumentError, fn -> tl(empty_list()) end
assert_raise ArgumentError, fn -> tl(Process.get(:unused, [])) end
assert tl([:a | :b]) == :b
assert tl([:a, :b | :c]) == [:b | :c]
@@ -1389,7 +1388,7 @@ defmodule KernelTest do
test "hd/1" do
assert hd([1, 2, 3, 4]) == 1
assert_raise ArgumentError, fn -> hd(empty_list()) end
assert_raise ArgumentError, fn -> hd(Process.get(:unused, [])) end
assert hd([1 | 2]) == 1
end
+5 -13
View File
@@ -6,8 +6,7 @@ defmodule MapTest do
doctest Map
@sample %{a: 1, b: 2}
defp sample, do: @sample
defp sample, do: Process.get(:unused, %{a: 1, b: 2})
test "maps in attributes" do
assert @sample == %{a: 1, b: 2}
@@ -82,7 +81,7 @@ defmodule MapTest do
test "map_size/1" do
assert map_size(%{}) == 0
assert map_size(@sample) == 2
assert map_size(sample()) == 2
end
test "new/1" do
@@ -282,8 +281,6 @@ defmodule MapTest do
defstruct name: "john", age: 27
end
defp empty_map(), do: %{}
test "structs" do
assert %ExternalUser{} == %{__struct__: ExternalUser, name: "john", age: 27}
@@ -294,10 +291,6 @@ defmodule MapTest do
%ExternalUser{name: name} = %ExternalUser{}
assert name == "john"
assert_raise BadStructError, "expected a struct named MapTest.ExternalUser, got: %{}", fn ->
%ExternalUser{empty_map() | name: "meg"}
end
end
describe "structs with variable name" do
@@ -325,12 +318,11 @@ defmodule MapTest do
end
end
defp foo(), do: "foo"
defp destruct1(%module{}), do: module
defp destruct2(%_{}), do: :ok
test "does not match" do
invalid_struct = %{__struct__: foo()}
invalid_struct = Process.get(:unused, %{__struct__: "foo"})
assert_raise CaseClauseError, fn ->
case invalid_struct do
@@ -345,7 +337,7 @@ defmodule MapTest do
end
assert_raise CaseClauseError, fn ->
foo = foo()
foo = Process.get(:unused, "foo")
case invalid_struct do
%^foo{} -> :ok
@@ -370,7 +362,7 @@ defmodule MapTest do
end
assert_raise MatchError, fn ->
foo = foo()
foo = Process.get(:unused, "foo")
%^foo{} = invalid_struct
end
end
@@ -1,135 +0,0 @@
Code.require_file("../test_helper.exs", __DIR__)
defmodule Module.LocalsTrackerTest do
use ExUnit.Case, async: true
alias Module.LocalsTracker, as: D
setup do
set = :ets.new(__MODULE__, [:set, :public])
bag = :ets.new(__MODULE__, [:duplicate_bag, :public])
[ref: {set, bag}]
end
## Locals
test "functions are reachable when connected through another one", config do
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
assert {:private, 1} in D.reachable_from(config[:ref], {:public, 1})
end
test "can yank and reattach nodes", config do
D.add_local(config[:ref], {:foo, 1}, {:bar, 1}, [line: 1], false)
outfoo = D.yank(config[:ref], {:foo, 1})
outbar = D.yank(config[:ref], {:bar, 1})
D.reattach(config[:ref], {:bar, 1}, :defp, {:bar, 1}, outbar, line: 2)
D.reattach(config[:ref], {:foo, 1}, :def, {:foo, 1}, outfoo, line: 3)
assert {:bar, 1} in D.reachable_from(config[:ref], {:foo, 1})
end
@used [
{{:public, 1}, :def, [], 0}
]
test "unused private definitions are marked as so", config do
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
unused = D.collect_unused_locals(config[:ref], @used, [{{:private, 0}, :defp, [], 0}])
assert unused == {[private: 0], [{[], {:unused_def, {:private, 0}, :defp}}]}
unused = D.collect_unused_locals(config[:ref], @used, [{{:private, 1}, :defp, [], 0}])
assert unused == {[], []}
end
@unused [
{{:private, 3}, :defp, [], 3}
]
test "preserves column information on retrieval", config do
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1, column: 1], false)
undefined = D.collect_undefined_locals(config[:ref], @used, "foo.exs")
assert undefined == [
{{:public, 1}, [span: {1, 8}, line: 1, column: 1], "foo.exs", {:private, 1},
:undefined_function}
]
end
test "private definitions with unused default arguments", config do
unused = D.collect_unused_locals(config[:ref], @used, @unused)
assert unused == {[private: 3], [{[], {:unused_def, {:private, 3}, :defp}}]}
D.add_local(config[:ref], {:public, 1}, {:private, 3}, [line: 1], false)
unused = D.collect_unused_locals(config[:ref], @used, @unused)
assert unused == {[], [{[], {:unused_args, {:private, 3}}}]}
end
test "private definitions with some unused default arguments", config do
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
unused = D.collect_unused_locals(config[:ref], @used, @unused)
assert unused == {[private: 3], [{[], {:unused_args, {:private, 3}, 1}}]}
end
test "private definitions with all used default arguments", config do
D.add_local(config[:ref], {:public, 1}, {:private, 0}, [line: 1], false)
unused = D.collect_unused_locals(config[:ref], @used, @unused)
assert unused == {[private: 3], []}
end
### Undefined functions
test "undefined functions are marked as so", config do
D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false)
undefined = D.collect_undefined_locals(config[:ref], @used, "foo.exs")
assert undefined == [{{:public, 1}, [line: 1], "foo.exs", {:private, 1}, :undefined_function}]
end
### Incorrect dispatches
test "incorrect dispatches are marked as so", config do
{set, _bag} = config[:ref]
:ets.insert(set, {{:def, {:macro, 1}}, :defmacro, [], "nofile", false, {0, true, 0}})
definitions = [{{:public, 1}, :def, [], 0}, {{:macro, 1}, :defmacro, [], 0}]
D.add_local(config[:ref], {:public, 1}, {:macro, 1}, [line: 5], false)
undefined = D.collect_undefined_locals(config[:ref], definitions, "foo.exs")
assert undefined == [{{:public, 1}, [line: 5], "foo.exs", {:macro, 1}, :incorrect_dispatch}]
end
## Defaults
test "defaults are connected to last clause only", config do
D.add_defaults(config[:ref], :defp, {:foo, 4}, 2, line: 1)
D.add_local(config[:ref], {:public, 1}, {:foo, 2}, [line: 2], false)
assert {:foo, 2} in D.reachable_from(config[:ref], {:public, 1})
refute {:foo, 3} in D.reachable_from(config[:ref], {:public, 1})
assert {:foo, 4} in D.reachable_from(config[:ref], {:public, 1})
end
## Imports
test "find import conflicts", config do
entries = [{{:conflict, 1}, :def, [], []}]
refute {[], {Module, {:conflict, 1}}} in D.collect_imports_conflicts(config[:ref], entries)
D.add_local(config[:ref], {:public, 1}, {:foo, 2}, [line: 1], false)
D.add_import(config[:ref], {:foo, 2}, Module, {:conflict, 1})
D.add_import(config[:ref], {:foo, 2}, Module, {:conflict, 1})
assert {[], {Module, {:conflict, 1}}} in D.collect_imports_conflicts(config[:ref], entries)
end
defmodule NoPrivate do
defmacrop foo(), do: bar()
defp bar(), do: :baz
def baz(), do: foo()
end
test "does not include unreachable locals" do
assert NoPrivate.module_info(:functions) |> Keyword.take([:foo, :bar, :"MACRO-foo"]) == []
end
end
@@ -61,4 +61,37 @@ defmodule Module.Types.InferTest do
{[dynamic(atom([:error]))], atom([:five])}
]}
end
test "infers return types from private functions", config do
types =
infer config do
def pub(x), do: priv(x)
defp priv(:ok), do: :ok
defp priv(:error), do: :error
end
assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic(atom([:ok, :error]))}]}
assert types[{:priv, 1}] == nil
end
test "infers return types from super functions", config do
types =
infer config do
def pub(:ok), do: :ok
def pub(:error), do: :error
defoverridable pub: 1
def pub(x), do: super(x)
end
assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic(atom([:ok, :error]))}]}
end
test "infers return types even with loops", config do
types =
infer config do
def pub(x), do: pub(x)
end
assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic()}]}
end
end
@@ -51,8 +51,10 @@ defmodule Module.Types.IntegrationTest do
{{:behaviour_info, 1}, %{sig: :none}}
]
end
end
test "type checks signatures" do
describe "type checking" do
test "inferred remote calls" do
files = %{
"a.ex" => """
defmodule A do
@@ -101,6 +103,98 @@ defmodule Module.Types.IntegrationTest do
assert_warnings(files, warnings)
end
test "mismatched locals" do
files = %{
"a.ex" => """
defmodule A do
def error(), do: private(raise "oops")
def public(x), do: private(List.to_tuple(x))
defp private(:ok), do: nil
end
"""
}
warnings = [
"""
warning: incompatible types given to private/1:
private(raise RuntimeError.exception("oops"))
""",
"the 1st argument is empty (often represented as none())",
"""
typing violation found at:
│
2 │ def error(), do: private(raise "oops")
│ ~
│
└─ a.ex:2:20: A.error/0
""",
"""
warning: incompatible types given to private/1:
private(List.to_tuple(x))
""",
"""
typing violation found at:
│
3 │ def public(x), do: private(List.to_tuple(x))
│ ~
│
└─ a.ex:3:22: A.public/1
"""
]
assert_warnings(files, warnings)
end
test "unused private clauses" do
files = %{
"a.ex" => """
defmodule A do
def public(x) do
private(List.to_tuple(x))
end
defp private(nil), do: nil
defp private("foo"), do: "foo"
defp private({:ok, ok}), do: ok
defp private({:error, error}), do: error
defp private("bar"), do: "bar"
end
"""
}
warnings = [
"""
warning: this clause of defp private/1 is never used
│
6 │ defp private(nil), do: nil
│ ~
│
└─ a.ex:6:8: A.private/1
""",
"""
warning: this clause of defp private/1 is never used
│
7 │ defp private("foo"), do: "foo"
│ ~
│
└─ a.ex:7:8: A.private/1
""",
"""
warning: this clause of defp private/1 is never used
│
10 │ defp private("bar"), do: "bar"
│ ~
│
└─ a.ex:10:8: A.private/1
"""
]
assert_warnings(files, warnings)
end
end
describe "undefined warnings" do
@@ -345,42 +439,6 @@ defmodule Module.Types.IntegrationTest do
assert_warnings(files, warnings)
end
test "protocols are checked, ignoring missing built-in impls" do
files = %{
"a.ex" => """
defprotocol AProtocol do
def func(arg)
end
defmodule AImplementation do
defimpl AProtocol do
def func(_), do: B.no_func()
end
end
"""
}
warnings = [
"B.no_func/0 is undefined (module B is not available or is yet to be defined)",
"a.ex:7:24: AProtocol.AImplementation.func/1"
]
assert_warnings(files, warnings)
end
test "handles Erlang ops" do
files = %{
"a.ex" => """
defmodule A do
def a(a, b), do: a and b
def b(a, b), do: a or b
end
"""
}
assert_no_warnings(files)
end
test "hints exclude deprecated functions" do
files = %{
"a.ex" => """
@@ -138,7 +138,8 @@ defmodule TypeHelper do
defp new_stack(mode) do
cache = if mode == :infer, do: :none, else: Module.ParallelChecker.test_cache()
Types.stack(mode, "types_test.ex", TypesTest, {:test, 0}, [], cache)
handler = fn _, _, _, _ -> raise "no local lookup" end
Types.stack(mode, "types_test.ex", TypesTest, {:test, 0}, [], cache, handler)
end
defp new_context() do
+3 -3
View File
@@ -314,13 +314,13 @@ defmodule ExUnit.AssertionsTest do
true = assert match?({2, 1}, Value.tuple())
try do
assert match?({:ok, _}, error(true))
assert match?({:ok, _}, Process.get(:unused, :ok))
flunk("This should never be tested")
rescue
error in [ExUnit.AssertionError] ->
"match (match?) failed" = error.message
"assert match?({:ok, _}, error(true))" = Macro.to_string(error.expr)
"{:error, true}" = Macro.to_string(error.right)
"assert match?({:ok, _}, Process.get(:unused, :ok))" = Macro.to_string(error.expr)
":ok" = Macro.to_string(error.right)
end
end
+6 -2
View File
@@ -446,10 +446,14 @@ defmodule ExUnit.CallbacksNoTests do
use ExUnit.Case, async: true
setup_all do
raise "never run"
if :rand.uniform() >= 0 do
raise "never run"
end
end
setup do
raise "never run"
if :rand.uniform() >= 0 do
raise "never run"
end
end
end