Skip to content

Commit e616166

Browse files
committed
Infer with built-ins
1 parent a1d491d commit e616166

File tree

6 files changed

+96
-46
lines changed

6 files changed

+96
-46
lines changed

lib/elixir/lib/module/types.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ defmodule Module.Types do
3333
@no_infer [__protocol__: 1, behaviour_info: 1]
3434

3535
@doc false
36-
def infer(module, file, defs, private, used_private, env) do
37-
infer_signatures? = :elixir_config.get(:infer_signatures)
36+
def infer(module, file, defs, private, used_private, env, {_, cache}) do
37+
infer_signatures? = :elixir_config.get(:infer_signatures) and cache != nil
3838

3939
finder =
4040
fn fun_arity ->
@@ -60,7 +60,7 @@ defmodule Module.Types do
6060
end
6161
end
6262

63-
stack = stack(:infer, file, module, {:__info__, 1}, :all, env, handler)
63+
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
6464

6565
{types, %{local_sigs: reachable_sigs} = context} =
6666
for {fun_arity, kind, meta, _clauses} = def <- defs,
@@ -316,9 +316,9 @@ defmodule Module.Types do
316316
module: module,
317317
# Current function
318318
function: function,
319-
# List of calls to not warn on as undefined or :all
319+
# List of calls to not warn on as undefined or :all or Macro.Env indicating limited remotes
320320
no_warn_undefined: no_warn_undefined,
321-
# A tuple with cache information or a Macro.Env struct indicating no remote traversals
321+
# A tuple with cache information (may be nil)
322322
cache: cache,
323323
# The mode to be used, see the @modes attribute
324324
mode: mode,

lib/elixir/lib/module/types/apply.ex

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,26 @@ defmodule Module.Types.Apply do
429429
end
430430
end
431431

432+
@doc """
433+
Returns the type of a remote capture.
434+
"""
435+
def remote_capture(modules, fun, arity, meta, stack, context) do
436+
# TODO: We cannot return the unions of functions. Do we forbid this?
437+
# Do we check it is always the same return type? Do we simply say it is a function?
438+
if stack.mode == :traversal do
439+
{dynamic(fun()), context}
440+
else
441+
context =
442+
Enum.reduce(
443+
modules,
444+
context,
445+
&(signature(&1, fun, arity, meta, stack, &2) |> elem(1))
446+
)
447+
448+
{dynamic(fun()), context}
449+
end
450+
end
451+
432452
@doc """
433453
Gets a mfa signature.
434454
@@ -461,25 +481,30 @@ defmodule Module.Types.Apply do
461481
{signature(:module_info, arity), context}
462482
end
463483

464-
defp export(_module, _fun, _arity, _meta, %{cache: %Macro.Env{}}, context) do
465-
{:none, context}
466-
end
484+
defp export(module, fun, arity, meta, %{cache: cache} = stack, context) do
485+
cond do
486+
cache == nil or stack.mode == :traversal ->
487+
{:none, context}
467488

468-
defp export(module, fun, arity, meta, stack, context) do
469-
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
470-
{:ok, mode, reason, info} ->
471-
info = if info == :none, do: signature(fun, arity), else: info
472-
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
489+
stack.mode == :infer and not builtin_module?(module) ->
490+
{:none, context}
473491

474-
{:error, type} ->
475-
context =
476-
if warn_undefined?(module, fun, arity, stack) do
477-
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
478-
else
479-
context
480-
end
492+
true ->
493+
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
494+
{:ok, mode, reason, info} ->
495+
info = if info == :none, do: signature(fun, arity), else: info
496+
{info, check_deprecated(mode, module, fun, arity, reason, meta, stack, context)}
481497

482-
{:none, context}
498+
{:error, type} ->
499+
context =
500+
if warn_undefined?(module, fun, arity, stack) do
501+
warn(__MODULE__, {:undefined, type, module, fun, arity}, meta, stack, context)
502+
else
503+
context
504+
end
505+
506+
{:none, context}
507+
end
483508
end
484509
end
485510

@@ -507,6 +532,27 @@ defmodule Module.Types.Apply do
507532
end
508533
end
509534

535+
defp builtin_module?(module) do
536+
is_map_key(builtin_modules(), module)
537+
end
538+
539+
defp builtin_modules do
540+
case :persistent_term.get(__MODULE__, nil) do
541+
nil ->
542+
{:ok, mods} = :application.get_key(:elixir, :modules)
543+
mods = Map.from_keys(mods, [])
544+
:persistent_term.put(__MODULE__, mods)
545+
mods
546+
547+
%{} = mods ->
548+
mods
549+
end
550+
end
551+
552+
defp warn_undefined?(_, _, _, %{no_warn_undefined: %Macro.Env{}}) do
553+
false
554+
end
555+
510556
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
511557
false
512558
end
@@ -572,14 +618,14 @@ defmodule Module.Types.Apply do
572618
{dynamic(fun()), context}
573619

574620
{_kind, _info, context} when stack.mode == :traversal ->
575-
{fun(), context}
621+
{dynamic(fun()), context}
576622

577623
{kind, _info, context} ->
578624
if stack.mode != :infer and kind == :defp do
579625
# Mark all clauses as used, as the function is being exported.
580-
{fun(), put_in(context.local_used[fun_arity], [])}
626+
{dynamic(fun()), put_in(context.local_used[fun_arity], [])}
581627
else
582-
{fun(), context}
628+
{dynamic(fun()), context}
583629
end
584630
end
585631
end

lib/elixir/lib/module/types/expr.ex

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -401,19 +401,8 @@ defmodule Module.Types.Expr do
401401
)
402402
when is_atom(name) and is_integer(arity) do
403403
{remote_type, context} = of_expr(remote, stack, context)
404-
405-
# TODO: We cannot return the unions of functions. Do we forbid this?
406-
# Do we check it is always the same return type? Do we simply say it is a function?
407404
{mods, context} = Of.modules(remote_type, name, arity, expr, meta, stack, context)
408-
409-
context =
410-
Enum.reduce(
411-
mods,
412-
context,
413-
&(Apply.signature(&1, name, arity, meta, stack, &2) |> elem(1))
414-
)
415-
416-
{dynamic(fun()), context}
405+
Apply.remote_capture(mods, name, arity, meta, stack, context)
417406
end
418407

419408
# TODO: &foo/1

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ defmodule Module.Types.Of do
216216
Returns `__info__(:struct)` information about a struct.
217217
"""
218218
def struct_info(struct, meta, stack, context) do
219-
case stack.cache do
219+
case stack.no_warn_undefined do
220220
%Macro.Env{} = env ->
221221
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
222222
{:ok, info} -> {info, context}

lib/elixir/src/elixir_module.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
188188
true -> {#{}, []};
189189
false ->
190190
UsedPrivate = bag_lookup_element(DataBag, used_private, 2),
191-
'Elixir.Module.Types':infer(Module, File, AllDefinitions, Private, UsedPrivate, E)
191+
'Elixir.Module.Types':infer(Module, File, AllDefinitions, Private, UsedPrivate, E, CheckerInfo)
192192
end,
193193

194194
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
@@ -225,7 +225,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
225225
elixir_env:trace({on_module, Binary, none}, ModuleE),
226226
warn_unused_attributes(DataSet, DataBag, PersistedAttributes, E),
227227
make_module_available(Module, Binary),
228-
(CheckerInfo == undefined) andalso
228+
(element(2, CheckerInfo) == nil) andalso
229229
[VerifyMod:VerifyFun(Module) ||
230230
{VerifyMod, VerifyFun} <- bag_lookup_element(DataBag, {accumulate, after_verify}, 2)],
231231
{module, Module, Binary, Result}
@@ -553,11 +553,11 @@ beam_location(ModuleAsCharlist) ->
553553

554554
checker_info() ->
555555
case get(elixir_checker_info) of
556-
undefined -> undefined;
556+
undefined -> {self(), nil};
557557
_ -> 'Elixir.Module.ParallelChecker':get()
558558
end.
559559

560-
spawn_parallel_checker(undefined, _Module, _ModuleMap) ->
560+
spawn_parallel_checker({_, nil}, _Module, _ModuleMap) ->
561561
ok;
562562
spawn_parallel_checker(CheckerInfo, Module, ModuleMap) ->
563563
Log =

lib/elixir/test/elixir/module/types/infer_test.exs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ defmodule Module.Types.InferTest do
77

88
defmacro infer(config, do: block) do
99
quote do
10-
defmodule unquote(config).test do
11-
unquote(block)
12-
end
13-
|> runtime_infer()
10+
runtime_infer(unquote(config).test, unquote(Macro.escape(block)))
1411
end
1512
end
1613

17-
defp runtime_infer({:module, module, binary, _result}) do
14+
defp runtime_infer(module, block) do
15+
{{:module, _, binary, _}, []} =
16+
Code.eval_quoted(
17+
quote do
18+
defmodule unquote(module), do: unquote(block)
19+
end,
20+
[]
21+
)
22+
1823
{:ok, {_, [debug_info: chunk]}} = :beam_lib.chunks(binary, [:debug_info])
1924
{:debug_info_v1, backend, data} = chunk
2025
{:ok, %{signatures: signatures}} = backend.debug_info(:elixir_v1, module, data, [])
@@ -43,6 +48,16 @@ defmodule Module.Types.InferTest do
4348
assert types[{:fun4, 4}] == {:infer, [{args, atom([:ok])}]}
4449
end
4550

51+
test "infer with Elixir built-in", config do
52+
types =
53+
infer config do
54+
def parse(string), do: Integer.parse(string)
55+
end
56+
57+
assert types[{:parse, 1}] ==
58+
{:infer, [{[dynamic()], dynamic(union(atom([:error]), tuple([integer(), term()])))}]}
59+
end
60+
4661
test "merges patterns", config do
4762
types =
4863
infer config do

0 commit comments

Comments
 (0)