diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 63ec7a2407d..7950607ad32 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -2547,61 +2547,3 @@ defmodule ErlangError do defp nth(3), do: "3rd" defp nth(n), do: "#{n}th" end - -defmodule Inspect.Error do - @moduledoc """ - Raised when a struct cannot be inspected. - """ - @enforce_keys [:exception_module, :exception_message, :stacktrace, :inspected_struct] - defexception @enforce_keys - - @impl true - def exception(arguments) when is_list(arguments) do - exception = Keyword.fetch!(arguments, :exception) - exception_module = exception.__struct__ - exception_message = Exception.message(exception) |> String.trim_trailing("\n") - stacktrace = Keyword.fetch!(arguments, :stacktrace) - inspected_struct = Keyword.fetch!(arguments, :inspected_struct) - - %Inspect.Error{ - exception_module: exception_module, - exception_message: exception_message, - stacktrace: stacktrace, - inspected_struct: inspected_struct - } - end - - @impl true - def message(%__MODULE__{ - exception_module: exception_module, - exception_message: exception_message, - inspected_struct: inspected_struct - }) do - ~s''' - got #{inspect(exception_module)} with message: - - """ - #{pad(exception_message, 4)} - """ - - while inspecting: - - #{pad(inspected_struct, 4)} - ''' - end - - @doc false - def pad(message, padding_length) - when is_binary(message) and is_integer(padding_length) and padding_length >= 0 do - padding = String.duplicate(" ", padding_length) - - message - |> String.split("\n") - |> Enum.map(fn - "" -> "\n" - line -> [padding, line, ?\n] - end) - |> IO.iodata_to_binary() - |> String.trim_trailing("\n") - end -end diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 43f747d3357..ee0639eca3b 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -577,36 +577,6 @@ defimpl Inspect, for: Function do end end -defimpl Inspect, for: Inspect.Error do - @impl true - def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do - message = Exception.message(inspect_error) - format_output(message, stacktrace) - end - - defp format_output(message, [_ | _] = stacktrace) do - stacktrace = Exception.format_stacktrace(stacktrace) - - """ - #Inspect.Error< - #{Inspect.Error.pad(message, 2)} - - Stacktrace: - - #{stacktrace} - >\ - """ - end - - defp format_output(message, []) do - """ - #Inspect.Error< - #{Inspect.Error.pad(message, 2)} - >\ - """ - end -end - defimpl Inspect, for: PID do def inspect(pid, _opts) do "#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid)) diff --git a/lib/elixir/lib/inspect/error.ex b/lib/elixir/lib/inspect/error.ex new file mode 100644 index 00000000000..64e65c08238 --- /dev/null +++ b/lib/elixir/lib/inspect/error.ex @@ -0,0 +1,87 @@ +defmodule Inspect.Error do + @moduledoc """ + Raised when a struct cannot be inspected. + """ + @enforce_keys [:exception_module, :exception_message, :stacktrace, :inspected_struct] + defexception @enforce_keys + + @impl true + def exception(arguments) when is_list(arguments) do + exception = Keyword.fetch!(arguments, :exception) + exception_module = exception.__struct__ + exception_message = Exception.message(exception) |> String.trim_trailing("\n") + stacktrace = Keyword.fetch!(arguments, :stacktrace) + inspected_struct = Keyword.fetch!(arguments, :inspected_struct) + + %Inspect.Error{ + exception_module: exception_module, + exception_message: exception_message, + stacktrace: stacktrace, + inspected_struct: inspected_struct + } + end + + @impl true + def message(%__MODULE__{ + exception_module: exception_module, + exception_message: exception_message, + inspected_struct: inspected_struct + }) do + ~s''' + got #{inspect(exception_module)} with message: + + """ + #{pad(exception_message, 4)} + """ + + while inspecting: + + #{pad(inspected_struct, 4)} + ''' + end + + @doc false + def pad(message, padding_length) + when is_binary(message) and is_integer(padding_length) and padding_length >= 0 do + padding = String.duplicate(" ", padding_length) + + message + |> String.split("\n") + |> Enum.map(fn + "" -> "\n" + line -> [padding, line, ?\n] + end) + |> IO.iodata_to_binary() + |> String.trim_trailing("\n") + end +end + +defimpl Inspect, for: Inspect.Error do + @impl true + def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do + message = Exception.message(inspect_error) + format_output(message, stacktrace) + end + + defp format_output(message, [_ | _] = stacktrace) do + stacktrace = Exception.format_stacktrace(stacktrace) + + """ + #Inspect.Error< + #{Inspect.Error.pad(message, 2)} + + Stacktrace: + + #{stacktrace} + >\ + """ + end + + defp format_output(message, []) do + """ + #Inspect.Error< + #{Inspect.Error.pad(message, 2)} + >\ + """ + end +end diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 5df93d783bf..fc9788f1d08 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -47,8 +47,17 @@ defmodule Module.ParallelChecker do @doc """ Spawns a process that runs the parallel checker. """ - def spawn({pid, {checker, table}}, module, module_map, log?) do - inner_spawn(pid, checker, table, module, cache_from_module_map(table, module_map), log?) + def spawn({pid, {checker, table}}, module, module_map, beam_location, log?) do + # Protocols may have been consolidated. So if we know their beam location, + # we discard their module map on purpose and start from file. + info = + if beam_location != [] and List.keymember?(module_map.attributes, :__protocol__, 0) do + List.to_string(beam_location) + else + cache_from_module_map(table, module_map) + end + + inner_spawn(pid, checker, table, module, info, log?) end defp inner_spawn(pid, checker, table, module, info, log?) do @@ -221,7 +230,7 @@ defmodule Module.ParallelChecker do ## Module checking defp check_module(module_tuple, cache, log?) do - {module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} = + {module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} = module_tuple behaviour_warnings = @@ -236,7 +245,7 @@ defmodule Module.ParallelChecker do diagnostics = module - |> Module.Types.warnings(file, definitions, no_warn_undefined, cache) + |> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache) |> Kernel.++(behaviour_warnings) |> group_warnings() |> emit_warnings(file, log?) @@ -273,7 +282,10 @@ defmodule Module.ParallelChecker do |> extract_no_warn_undefined() |> merge_compiler_no_warn_undefined() - {module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} + attributes = Keyword.take(attributes, [:__protocol__, :__impl__]) + + {module, file, line, definitions, no_warn_undefined, behaviours, impls, attributes, + after_verify} end defp extract_no_warn_undefined(compile_opts) do diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index aa704982096..3f77c61a4ed 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -30,17 +30,26 @@ defmodule Module.Types do @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] + @no_infer [behaviour_info: 1] @doc false - def infer(module, file, defs, private, used_private, env, {_, cache}) do - infer_signatures? = :elixir_config.get(:infer_signatures) and cache != nil + def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do + # We don't care about inferring signatures for protocols, + # those will be replaced anyway. There is also nothing to + # infer if there is no cache system, we only do traversals. + infer_signatures? = + :elixir_config.get(:infer_signatures) and cache != nil and not protocol?(attrs) + + impl = impl_for(attrs) finder = fn fun_arity -> case :lists.keyfind(fun_arity, 1, defs) do - {_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause} - false -> false + {_, kind, _, _} = clause -> + {infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)} + + false -> + false end end @@ -67,7 +76,11 @@ defmodule Module.Types do kind in [:def, :defmacro], reduce: {[], context()} do {types, context} -> - finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end + # Optimized version of finder, since we already the definition + finder = fn _ -> + {infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)} + 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 @@ -111,6 +124,33 @@ defmodule Module.Types do if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal end + defp protocol?(attrs) do + List.keymember?(attrs, :__protocol__, 0) + end + + defp impl_for(attrs) do + case List.keyfind(attrs, :__impl__, 0) do + {:__impl__, [protocol: protocol, for: for]} -> + if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do + {for, protocol.behaviour_info(:callbacks)} + else + nil + end + + _ -> + nil + end + end + + defp default_domain({_, arity} = fun_arity, impl) do + with {for, callbacks} <- impl, + true <- fun_arity in callbacks do + [Module.Types.Of.impl(for) | List.duplicate(Descr.dynamic(), arity - 1)] + else + _ -> List.duplicate(Descr.dynamic(), arity) + end + 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} @@ -159,10 +199,12 @@ defmodule Module.Types do end @doc false - def warnings(module, file, defs, no_warn_undefined, cache) do + def warnings(module, file, attrs, defs, no_warn_undefined, cache) do + impl = impl_for(attrs) + finder = fn fun_arity -> case :lists.keyfind(fun_arity, 1, defs) do - {_, _, _, _} = clause -> {:dynamic, clause} + {_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)} false -> false end end @@ -172,7 +214,8 @@ defmodule Module.Types do context = Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context -> - finder = fn _ -> {:dynamic, def} end + # Optimized version of finder, since we already the definition + finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end {_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder) context end) @@ -211,11 +254,11 @@ defmodule Module.Types do local_sigs -> case finder.(fun_arity) do - {mode, {fun_arity, kind, meta, clauses}} -> + {mode, {fun_arity, kind, meta, clauses}, expected} -> 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) + local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) context = update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping})) @@ -228,8 +271,8 @@ defmodule Module.Types do end end - defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do - expected = List.duplicate(Descr.dynamic(), arity) + defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do + {fun, _arity} = fun_arity stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta) {_, _, mapping, clauses_types, clauses_context} = diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index a5bdff26995..cb10c78042b 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -87,6 +87,40 @@ defmodule Module.Types.Of do defp new_trace(expr, type, formatter, stack, traces), do: [{expr, stack.file, type, formatter} | traces] + ## Implementations + + # Right now we are still defaulting all implementations to their dynamic variations. + # TODO: What should the default types be once we have typed protocols? + + impls = [ + {Atom, atom()}, + {BitString, binary()}, + {Float, float()}, + {Function, fun()}, + {Integer, integer()}, + {List, list(term())}, + {Map, open_map()}, + {Port, port()}, + {PID, pid()}, + {Reference, reference()}, + {Tuple, tuple()}, + {Any, term()} + ] + + for {for, type} <- impls do + def impl(unquote(for)), do: unquote(Macro.escape(dynamic(type))) + end + + def impl(struct) do + # Elixir did not strictly require the implementation to be available, so we need a fallback. + # TODO: Assume implementation is available on Elixir v2.0. + if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do + dynamic(struct_type(struct, info)) + else + dynamic(open_map(__struct__: atom([struct]))) + end + end + ## Map/structs @doc """ diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 247b2b59e27..c492a7041f4 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -600,7 +600,7 @@ defmodule Protocol do # impl_for/1 dispatch version. defp change_debug_info(protocol, {any, definitions}, types) do types = if any, do: types, else: List.delete(types, Any) - all = [Any] ++ for {_guard, mod} <- __built_in__(), do: mod + all = [Any] ++ for {mod, _guard} <- built_in(), do: mod structs = types -- all case List.keytake(definitions, {:__protocol__, 1}, 0) do @@ -635,7 +635,7 @@ defmodule Protocol do line = meta[:line] clauses = - for {guard, mod} <- __built_in__(), + for {mod, guard} <- built_in(), mod in types, do: built_in_clause_for(mod, guard, protocol, meta, line) @@ -804,7 +804,7 @@ defmodule Protocol do end defp after_defprotocol do - quote bind_quoted: [built_in: __built_in__()] do + quote bind_quoted: [built_in: built_in()] do any_impl_for = if @fallback_to_any do __MODULE__.Any @@ -830,7 +830,7 @@ defmodule Protocol do # Define the implementation for built-ins :lists.foreach( - fn {guard, mod} -> + fn {mod, guard} -> target = Module.concat(__MODULE__, mod) Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do @@ -951,7 +951,7 @@ defmodule Protocol do name = Module.concat(protocol, for) Protocol.assert_protocol!(protocol) - Protocol.__ensure_defimpl__(protocol, for, __ENV__) + Protocol.__impl__!(protocol, for, __ENV__) defmodule name do @moduledoc false @@ -1004,7 +1004,7 @@ defmodule Protocol do if function_exported?(mod, fun, length(args)) do apply(mod, fun, args) else - __ensure_defimpl__(protocol, for, env) + __impl__!(protocol, for, env) assert_impl!(protocol, Any, extra) impl = Module.concat(protocol, Any) @@ -1037,7 +1037,7 @@ defmodule Protocol do end @doc false - def __ensure_defimpl__(protocol, for, env) do + def __impl__!(protocol, for, env) do if not Code.get_compiler_option(:ignore_already_consolidated) and Protocol.consolidated?(protocol) do message = @@ -1049,25 +1049,34 @@ defmodule Protocol do IO.warn(message, env) end + # TODO: Make this an error on Elixir v2.0 + if for != Any and not Keyword.has_key?(built_in(), for) and for != env.module and + for not in env.context_modules and + not Code.ensure_loaded?(for) do + IO.warn( + "you are implementing a protocol for #{inspect(for)} but said module is not available. " <> + "Make sure the module name is correct. If #{inspect(for)} is an optional dependency, " <> + "please wrap the protocol implementation in a Code.ensure_loaded?(#{inspect(for)}) check", + env + ) + end + :ok end - ## Helpers - - @doc false - def __built_in__ do + defp built_in do [ - is_tuple: Tuple, - is_atom: Atom, - is_list: List, - is_map: Map, - is_bitstring: BitString, - is_integer: Integer, - is_float: Float, - is_function: Function, - is_pid: PID, - is_port: Port, - is_reference: Reference + {Tuple, :is_tuple}, + {Atom, :is_atom}, + {List, :is_list}, + {Map, :is_map}, + {BitString, :is_bitstring}, + {Integer, :is_integer}, + {Float, :is_float}, + {Function, :is_function}, + {PID, :is_pid}, + {Port, :is_port}, + {Reference, :is_reference} ] end end diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 8b484d42392..459ffc7cd29 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -155,6 +155,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> put_compiler_modules([Module | CompilerModules]), {Result, ModuleE, CallbackE} = eval_form(Line, Module, DataBag, Block, Vars, Prune, E), CheckerInfo = checker_info(), + BeamLocation = beam_location(ModuleAsCharlist), {Binary, PersistedAttributes, Autoload} = elixir_erl_compiler:spawn(fun() -> @@ -188,7 +189,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> true -> {#{}, []}; false -> UsedPrivate = bag_lookup_element(DataBag, used_private, 2), - 'Elixir.Module.Types':infer(Module, File, AllDefinitions, Private, UsedPrivate, E, CheckerInfo) + 'Elixir.Module.Types':infer(Module, File, Attributes, AllDefinitions, Private, UsedPrivate, E, CheckerInfo) end, RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2), @@ -215,11 +216,11 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> compile_error_if_tainted(DataSet, E), Binary = elixir_erl:compile(ModuleMap), Autoload = proplists:get_value(autoload, CompileOpts, true), - spawn_parallel_checker(CheckerInfo, Module, ModuleMap), + spawn_parallel_checker(CheckerInfo, Module, ModuleMap, BeamLocation), {Binary, PersistedAttributes, Autoload} end), - Autoload andalso code:load_binary(Module, beam_location(ModuleAsCharlist), Binary), + Autoload andalso code:load_binary(Module, BeamLocation, Binary), put_compiler_modules(CompilerModules), eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE), elixir_env:trace({on_module, Binary, none}, ModuleE), @@ -558,15 +559,15 @@ checker_info() -> _ -> 'Elixir.Module.ParallelChecker':get() end. -spawn_parallel_checker({_, nil}, _Module, _ModuleMap) -> +spawn_parallel_checker({_, nil}, _Module, _ModuleMap, _BeamLocation) -> ok; -spawn_parallel_checker(CheckerInfo, Module, ModuleMap) -> +spawn_parallel_checker(CheckerInfo, Module, ModuleMap, BeamLocation) -> Log = case erlang:get(elixir_code_diagnostics) of {_, false} -> false; _ -> true end, - 'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Log). + 'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, BeamLocation, Log). make_module_available(Module, Binary) -> case get(elixir_module_binaries) of diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 943c18f7d8c..386ad37cb57 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -4,6 +4,7 @@ defmodule Module.Types.IntegrationTest do use ExUnit.Case import ExUnit.CaptureIO + import Module.Types.Descr setup_all do previous = Application.get_env(:elixir, :ansi_enabled, false) @@ -51,6 +52,72 @@ defmodule Module.Types.IntegrationTest do {{:behaviour_info, 1}, %{sig: :none}} ] end + + test "writes exports for inferred protocols and implementations" do + files = %{ + "pi.ex" => """ + defprotocol Itself do + @fallback_to_any true + def itself(data) + end + + defimpl Itself, + for: [ + Atom, + BitString, + Float, + Function, + Integer, + List, + Map, + Port, + PID, + Reference, + Tuple, + Any, + Range, + Unknown + ] do + def itself(data), do: data + def this_wont_warn(:ok), do: :ok + end + """ + } + + {modules, stderr} = with_io(:stderr, fn -> compile_modules(files) end) + + assert stderr =~ + "you are implementing a protocol for Unknown but said module is not available" + + refute stderr =~ "this_wont_warn" + + itself_arg = fn mod -> + {_, %{sig: {:infer, [{[value], value}]}}} = + List.keyfind(read_chunk(modules[mod]).exports, {:itself, 1}, 0) + + value + end + + assert itself_arg.(Itself.Atom) == dynamic(atom()) + assert itself_arg.(Itself.BitString) == dynamic(binary()) + assert itself_arg.(Itself.Float) == dynamic(float()) + assert itself_arg.(Itself.Function) == dynamic(fun()) + assert itself_arg.(Itself.Integer) == dynamic(integer()) + assert itself_arg.(Itself.List) == dynamic(list(term())) + assert itself_arg.(Itself.Map) == dynamic(open_map()) + assert itself_arg.(Itself.Port) == dynamic(port()) + assert itself_arg.(Itself.PID) == dynamic(pid()) + assert itself_arg.(Itself.Reference) == dynamic(reference()) + assert itself_arg.(Itself.Tuple) == dynamic(tuple()) + assert itself_arg.(Itself.Any) == dynamic(term()) + + assert itself_arg.(Itself.Range) == + dynamic( + closed_map(__struct__: atom([Range]), first: term(), last: term(), step: term()) + ) + + assert itself_arg.(Itself.Unknown) == dynamic(open_map(__struct__: atom([Unknown]))) + end end describe "type checking" do