Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/elixir/lib/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ defmodule Code do
:debug_info,
:ignore_already_consolidated,
:ignore_module_conflict,
:infer_signatures,
:relative_paths,
:warnings_as_errors
]
Expand Down Expand Up @@ -1638,6 +1639,10 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`.

* `:infer_signatures` (since v1.18.0) - when `false`, it disables inference
module-local signatures used when type checking remote calls to the compiled
module. Type checking it still executed. Defaults to `true`.

* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
Expand Down
28 changes: 16 additions & 12 deletions lib/elixir/lib/module/parallel_checker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ defmodule Module.ParallelChecker do
or if the function does not exist return `{:error, :function}`.
"""
@spec fetch_export(cache(), module(), atom(), arity()) ::
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
| {:error, :function | :module}
def fetch_export({server, ets}, module, fun, arity) do
case :ets.lookup(ets, module) do
[] ->
Expand All @@ -203,7 +204,7 @@ defmodule Module.ParallelChecker do

[{_key, mode}] ->
case :ets.lookup(ets, {module, {fun, arity}}) do
[{_key, reason}] -> {:ok, mode, reason}
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
[] -> {:error, :function}
end
end
Expand Down Expand Up @@ -369,13 +370,13 @@ defmodule Module.ParallelChecker do
true ->
{mode, exports} = info_exports(module)
deprecated = info_deprecated(module)
cache_info(ets, module, exports, deprecated, mode)
cache_info(ets, module, exports, deprecated, %{}, mode)

false ->
# Or load exports from chunk
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
cache_info(ets, module, exports, %{}, :erlang)
cache_info(ets, module, exports, %{}, %{}, :erlang)
else
_ ->
:ets.insert(ets, {module, false})
Expand Down Expand Up @@ -417,25 +418,28 @@ defmodule Module.ParallelChecker do
behaviour_exports(map) ++
for({function, :def, _meta, _clauses} <- map.definitions, do: function)

deprecated = Map.new(map.deprecated)
cache_info(ets, map.module, exports, deprecated, :elixir)
cache_info(ets, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
end

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

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

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

:ets.insert(ets, {{module, {:__info__, 1}}, nil})
:ets.insert(ets, {{module, {:__info__, 1}}, nil, :none})
:ets.insert(ets, {module, :elixir})
end

Expand Down
98 changes: 68 additions & 30 deletions lib/elixir/lib/module/types.ex
Original file line number Diff line number Diff line change
@@ -1,37 +1,64 @@
defmodule Module.Types do
@moduledoc false

alias Module.Types.{Expr, Pattern}
alias Module.Types.{Descr, Expr, Pattern}

@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
def infer(module, file, defs, env) do
context = context()

Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
stack =
stack(:dynamic, with_file_meta(meta, file), module, function, no_warn_undefined, cache)
for {{fun, arity}, :def, _meta, clauses} <- defs, into: %{} do
stack = stack(:infer, file, module, {fun, arity}, :all, env)
expected = List.duplicate(Descr.dynamic(), arity)

pair_types =
Enum.reduce(clauses, [], fn {meta, args, guards, body}, inferred ->
try do
{args, 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)

# TODO: Reuse context from patterns and guards
{{fun, arity}, {:infer, Enum.reverse(pair_types)}}
end
end

Enum.flat_map(clauses, fn {meta, args, guards, body} ->
try do
warnings_from_clause(meta, args, guards, body, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
# 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])

error =
RuntimeError.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
defp add_inferred([head | tail], args, return, acc),
do: add_inferred(tail, args, return, [head | acc])

#{Exception.format_banner(:error, e, __STACKTRACE__)}\
defp add_inferred([], args, return, acc),
do: [{args, return} | Enum.reverse(acc)]

The exception happened while checking this code:
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
context = context()

#{Macro.to_string(def_expr)}
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)

Please report this bug at: https://github.com/elixir-lang/elixir/issues
""")
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
try do
{_types, context} =
Pattern.of_head(args, guards, expected, :default, meta, stack, context)

reraise error, __STACKTRACE__
{_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)
Expand All @@ -44,6 +71,25 @@ defmodule Module.Types do
end
end

defp internal_error!(e, stack, kind, meta, module, fun, args, guards, body) 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))}:

#{Exception.format_banner(:error, e, stack)}\

The exception happened while checking this code:

#{Macro.to_string(def_expr)}

Please report this bug at: https://github.com/elixir-lang/elixir/issues
""")

reraise exception, stack
end

defp guards_to_expr([], left) do
left
end
Expand All @@ -52,14 +98,6 @@ defmodule Module.Types do
guards_to_expr(guards, {:when, [], [left, guard]})
end

defp warnings_from_clause(meta, args, guards, body, stack, context) do
dynamic = Module.Types.Descr.dynamic()
expected = Enum.map(args, fn _ -> dynamic end)
{_types, context} = Pattern.of_head(args, guards, expected, :default, meta, stack, context)
{_type, context} = Expr.of_expr(body, stack, context)
context.warnings
end

@doc false
def stack(mode, file, module, function, no_warn_undefined, cache)
when mode in [:static, :dynamic, :infer] do
Expand All @@ -72,9 +110,9 @@ defmodule Module.Types do
module: module,
# Current function
function: function,
# List of calls to not warn on as undefined
# List of calls to not warn on as undefined or :all
no_warn_undefined: no_warn_undefined,
# A list of cached modules received from the parallel compiler
# 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
Expand Down
14 changes: 11 additions & 3 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1145,9 +1145,17 @@ defmodule Module.Types.Descr do

defp dynamic_to_quoted(descr) do
cond do
term_type?(descr) -> [{:dynamic, [], []}]
single = indivisible_bitmap(descr) -> [single]
true -> [{:dynamic, [], [to_quoted(descr)]}]
term_type?(descr) ->
[{:dynamic, [], []}]

single = indivisible_bitmap(descr) ->
[single]

true ->
case to_quoted(descr) do
{:none, _meta, []} = none -> [none]
descr -> [{:dynamic, [], [descr]}]
end
end
end

Expand Down
9 changes: 9 additions & 0 deletions lib/elixir/lib/module/types/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,15 @@ defmodule Module.Types.Helpers do
|> Macro.to_string()
end

defp erl_to_ex(
:erlang,
:error,
[expr, :none, [error_info: {:%{}, _, [module: Exception]}]],
meta
) do
{:raise, meta, [expr]}
end

defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args, _} -> {fun, meta, args}
Expand Down
68 changes: 58 additions & 10 deletions lib/elixir/lib/module/types/of.ex
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,19 @@ defmodule Module.Types.Of do
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, meta, stack, context) do
{_, context} = remote(struct, :__struct__, 0, meta, stack, context)
case stack.cache do
%Macro.Env{} = env ->
{Macro.struct_info!(struct, env), context}

info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
_ ->
{_, context} = export(struct, :__struct__, 0, meta, stack, context)

info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"

{info, context}
{info, context}
end
end

@doc """
Expand Down Expand Up @@ -803,6 +809,21 @@ defmodule Module.Types.Of do
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)
Expand All @@ -826,11 +847,15 @@ defmodule Module.Types.Of 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} ->
{remote(fun, arity),
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
{: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 =
Expand Down Expand Up @@ -1051,6 +1076,21 @@ defmodule Module.Types.Of do
{{:., _, [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 subsequent function call with \
said value to always fail
"""
else
"""
but expected one of:
#{clauses_args_to_quoted_string(clauses, converter)}
"""
end

%{
details: %{typing_traces: traces},
message:
Expand All @@ -1064,9 +1104,8 @@ defmodule Module.Types.Of do

#{args_to_quoted_string(args_types, domain, converter) |> indent(4)}

but expected one of:
#{clauses_args_to_quoted_string(clauses, converter)}
""",
explanation,
format_traces(traces)
])
}
Expand Down Expand Up @@ -1253,4 +1292,13 @@ defmodule Module.Types.Of do
single_line -> binary_slice(single_line, 1..-2//1)
end
end

defp integer_to_ordinal(i) do
case rem(i, 10) in [1, 2, 3] 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
1 change: 1 addition & 0 deletions lib/elixir/src/elixir.erl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ start(_Type, _Args) ->

%% Compiler options
{docs, true},
{infer_signatures, true},
{ignore_already_consolidated, false},
{ignore_module_conflict, false},
{on_undefined_variable, raise},
Expand Down
Loading
Loading