diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 290e8b2527..43f747d335 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -139,6 +139,85 @@ defprotocol Inspect do # Handle structs in Any @fallback_to_any true + @impl true + defmacro __deriving__(module, options) do + info = Macro.struct_info!(module, __CALLER__) + fields = Enum.sort(Enum.map(info, & &1.field) -- [:__exception__, :__struct__]) + + only = Keyword.get(options, :only, fields) + except = Keyword.get(options, :except, []) + optional = Keyword.get(options, :optional, []) + + :ok = validate_option(:only, only, fields, module) + :ok = validate_option(:except, except, fields, module) + :ok = validate_option(:optional, optional, fields, module) + + inspect_module = + if fields == Enum.sort(only) and except == [] do + Inspect.Map + else + Inspect.Any + end + + filtered_fields = + fields + |> Enum.reject(&(&1 in except)) + |> Enum.filter(&(&1 in only)) + + filtered_guard = + quote do + var!(field) in unquote(filtered_fields) + end + + field_guard = + if optional == [] do + filtered_guard + else + optional_map = + for field <- optional, into: %{} do + default = Enum.find(info, %{}, &(&1.field == field)) |> Map.get(:default, nil) + {field, default} + end + + quote do + unquote(filtered_guard) and + not case unquote(Macro.escape(optional_map)) do + %{^var!(field) => var!(default)} -> + var!(default) == Map.get(var!(struct), var!(field)) + + %{} -> + false + end + end + end + + quote do + defimpl Inspect, for: unquote(module) do + def inspect(var!(struct), var!(opts)) do + var!(infos) = + for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct), + unquote(field_guard), + do: var!(info) + + var!(name) = Macro.inspect_atom(:literal, unquote(module)) + unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts)) + end + end + end + end + + defp validate_option(option, option_list, fields, module) do + case option_list -- fields do + [] -> + :ok + + unknown_fields -> + raise ArgumentError, + "unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <> + "when deriving the Inspect protocol for #{Kernel.inspect(module)}" + end + end + @doc """ Converts `term` into an algebra document. @@ -548,79 +627,6 @@ defimpl Inspect, for: Reference do end defimpl Inspect, for: Any do - defmacro __deriving__(module, struct, options) do - fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__]) - - only = Keyword.get(options, :only, fields) - except = Keyword.get(options, :except, []) - optional = Keyword.get(options, :optional, []) - - :ok = validate_option(:only, only, fields, module) - :ok = validate_option(:except, except, fields, module) - :ok = validate_option(:optional, optional, fields, module) - - inspect_module = - if fields == Enum.sort(only) and except == [] do - Inspect.Map - else - Inspect.Any - end - - filtered_fields = - fields - |> Enum.reject(&(&1 in except)) - |> Enum.filter(&(&1 in only)) - - filtered_guard = - quote do - var!(field) in unquote(filtered_fields) - end - - field_guard = - if optional == [] do - filtered_guard - else - optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)} - - quote do - unquote(filtered_guard) and - not case unquote(Macro.escape(optional_map)) do - %{^var!(field) => var!(default)} -> - var!(default) == Map.get(var!(struct), var!(field)) - - %{} -> - false - end - end - end - - quote do - defimpl Inspect, for: unquote(module) do - def inspect(var!(struct), var!(opts)) do - var!(infos) = - for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct), - unquote(field_guard), - do: var!(info) - - var!(name) = Macro.inspect_atom(:literal, unquote(module)) - unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts)) - end - end - end - end - - defp validate_option(option, option_list, fields, module) do - case option_list -- fields do - [] -> - :ok - - unknown_fields -> - raise ArgumentError, - "unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <> - "when deriving the Inspect protocol for #{Kernel.inspect(module)}" - end - end - def inspect(%module{} = struct, opts) do try do {module.__struct__(), module.__info__(:struct)} diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index f19ad4a0d0..2d5cb0716c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5406,12 +5406,12 @@ defmodule Kernel do defstruct name: nil, age: nil end - For each protocol in `@derive`, Elixir will assert the protocol has - been implemented for `Any`. If the `Any` implementation defines a - `__deriving__/3` callback, the callback will be invoked and it should define - the implementation module. Otherwise an implementation that simply points to - the `Any` implementation is automatically derived. For more information on - the `__deriving__/3` callback, see `Protocol.derive/3`. + For each protocol in `@derive`, Elixir will verify if the protocol + has implemented the `c:Protocol.__deriving__/2` callback. If so, + the callback will be invoked and it should define the implementation + module. Otherwise an implementation that simply points to the `Any` + implementation is automatically derived. For more information, see + `Protocol.derive/3`. ## Enforcing keys diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 9302f0e65c..251be8c19d 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -213,7 +213,7 @@ defmodule Kernel.Utils do case enforce_keys -- :maps.keys(struct) do [] -> mapper = fn {key, val} -> - %{field: key, default: val, required: :lists.member(key, enforce_keys)} + %{field: key, default: val} end :ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)}) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index ce1aa85574..b31eeb3203 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -704,7 +704,8 @@ defmodule Module do @callback __info__(:macros) :: keyword() @callback __info__(:md5) :: binary() @callback __info__(:module) :: module() - @callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil + @callback __info__(:struct) :: + list(%{required(:field) => atom(), optional(:default) => term()}) | nil @doc """ Returns information about module attributes used by Elixir. diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index f5cd6b49fe..b8a19022e3 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -54,7 +54,7 @@ defmodule Module.Types.Apply do functions: fas, macros: fas, struct: - list(closed_map(default: term(), field: atom(), required: boolean())) + list(closed_map(default: if_set(term()), field: atom())) |> union(atom([nil])) ] ++ shared_info, __protocol__: [ diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 9d52ae6a1d..941d8b739b 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -175,57 +175,6 @@ defmodule Protocol do to be used on `Protocol.UndefinedError` when looking up the implementation fails. This option is only applied if `@fallback_to_any` is not set to true - ## Reflection - - Any protocol module contains three extra functions: - - * `__protocol__/1` - returns the protocol information. The function takes - one of the following atoms: - - * `:consolidated?` - returns whether the protocol is consolidated - - * `:functions` - returns a keyword list of protocol functions and their arities - - * `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules - implementing the protocol, otherwise `:not_consolidated` - - * `:module` - the protocol module atom name - - * `impl_for/1` - returns the module that implements the protocol for the given argument, - `nil` otherwise - - * `impl_for!/1` - same as above but raises `Protocol.UndefinedError` if an implementation is - not found - - For example, for the `Enumerable` protocol we have: - - iex> Enumerable.__protocol__(:functions) - [count: 1, member?: 2, reduce: 3, slice: 1] - - iex> Enumerable.impl_for([]) - Enumerable.List - - iex> Enumerable.impl_for(42) - nil - - In addition, every protocol implementation module contains the `__impl__/1` - function. The function takes one of the following atoms: - - * `:for` - returns the module responsible for the data structure of the - protocol implementation - - * `:protocol` - returns the protocol module for which this implementation - is provided - - For example, the module implementing the `Enumerable` protocol for lists is - `Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module: - - iex(1)> Enumerable.List.__impl__(:for) - List - - iex(2)> Enumerable.List.__impl__(:protocol) - Enumerable - ## Consolidation In order to speed up protocol dispatching, whenever all protocol implementations @@ -279,6 +228,41 @@ defmodule Protocol do globally set. """ + @doc """ + A function available in all protocol definitions that returns protocol metadata. + """ + @callback __protocol__(:consolidated?) :: boolean() + @callback __protocol__(:functions) :: [{atom(), arity()}] + @callback __protocol__(:impls) :: {:consolidated, [module()]} | :not_consolidated + @callback __protocol__(:module) :: module + + @doc """ + A function available in all protocol definitions that returns the implementation + for the given `term` or nil. + + If `@fallback_to_any` is true, `nil` is never returned. + """ + @callback impl_for(term) :: module() | nil + + @doc """ + A function available in all protocol definitions that returns the implementation + for the given `term` or raises. + + If `@fallback_to_any` is true, it never raises. + """ + @callback impl_for!(term) :: module() + + @doc """ + An optional callback to be implemented by protocol authors for custom deriving. + + It must return a quoted expression that implements the protocol for the given module. + + See `Protocol.derive/3` for an example. + """ + @macrocallback __deriving__(module(), term()) :: Macro.t() + + @optional_callbacks __deriving__: 2 + @doc false defmacro def(signature) @@ -391,52 +375,50 @@ defmodule Protocol do @doc """ Derives the `protocol` for `module` with the given options. - If your implementation passes options or if you are generating - custom code based on the struct, you will also need to implement - a macro defined as `__deriving__(module, struct, options)` - to get the options that were passed. + Every time you derive a protocol, Elixir will verify if the protocol + has implemented the `c:Protocol.__deriving__/2` callback. If so, + the callback will be invoked and it should define the implementation + module. Otherwise an implementation that simply points to the `Any` + implementation is automatically derived. ## Examples defprotocol Derivable do - def ok(arg) - end + @impl true + defmacro __deriving__(module, options) do + # If you need to load struct metadata, you may call: + # struct_info = Macro.struct_info!(module, __CALLER__) - defimpl Derivable, for: Any do - defmacro __deriving__(module, struct, options) do quote do defimpl Derivable, for: unquote(module) do def ok(arg) do - {:ok, arg, unquote(Macro.escape(struct)), unquote(options)} + {:ok, arg, unquote(options)} end end end end - def ok(arg) do - {:ok, arg} - end + def ok(arg) end + Once the protocol is defined, there are two ways it can be + derived. The first is by using the `@derive` module attribute + by the time you define the struct: + defmodule ImplStruct do @derive [Derivable] defstruct a: 0, b: 0 end Derivable.ok(%ImplStruct{}) - #=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []} - - Explicit derivations can now be called via `__deriving__/3`: + #=> {:ok, %ImplStruct{a: 0, b: 0}, []} - # Explicitly derived via `__deriving__/3` - Derivable.ok(%ImplStruct{a: 1, b: 1}) - #=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []} + If the struct has already been defined, you can call this macro: - # Explicitly derived by API via `__deriving__/3` require Protocol Protocol.derive(Derivable, ImplStruct, :oops) Derivable.ok(%ImplStruct{a: 1, b: 1}) - #=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops} + #=> {:ok, %ImplStruct{a: 1, b: 1}, :oops} """ defmacro derive(protocol, module, options \\ []) do @@ -711,25 +693,14 @@ defmodule Protocol do def __protocol__(name, do: block) do quote do defmodule unquote(name) do + @behaviour Protocol @before_compile Protocol # We don't allow function definition inside protocols import Kernel, - except: [ - def: 1, - def: 2, - defp: 1, - defp: 2, - defdelegate: 2, - defguard: 1, - defguardp: 1, - defmacro: 1, - defmacro: 2, - defmacrop: 1, - defmacrop: 2 - ] - - # Import the new dsl that holds the new def + except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1] + + # Import the new `def` that is used by protocols import Protocol, only: [def: 1] # Compile with debug info for consolidation @@ -748,19 +719,17 @@ defmodule Protocol do end end - defp callback_ast_to_fa({kind, {:"::", meta, [{name, _, args}, _return]}, _pos}) - when kind in [:callback, :macrocallback] do + defp callback_ast_to_fa({_kind, {:"::", meta, [{name, _, args}, _return]}, _pos}) do [{{name, length(List.wrap(args))}, meta}] end defp callback_ast_to_fa( - {kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos} - ) - when kind in [:callback, :macrocallback] do + {_kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos} + ) do [{{name, length(List.wrap(args))}, meta}] end - defp callback_ast_to_fa({kind, _, _pos}) when kind in [:callback, :macrocallback] do + defp callback_ast_to_fa({_kind, _clause, _pos}) do [] end @@ -778,13 +747,10 @@ defmodule Protocol do end defp warn(message, env, line) when is_integer(line) do - stacktrace = :maps.update(:line, line, env) |> Macro.Env.stacktrace() - IO.warn(message, stacktrace) + IO.warn(message, %{env | line: line}) end def __before_compile__(env) do - callback_metas = callback_metas(env.module, :callback) - callbacks = :maps.keys(callback_metas) functions = Module.get_attribute(env.module, :__functions__) if functions == [] do @@ -795,8 +761,11 @@ defmodule Protocol do ) end + callback_metas = callback_metas(env.module, :callback) + callbacks = :maps.keys(callback_metas) + # TODO: Convert the following warnings into errors in future Elixir versions - :lists.map( + :lists.foreach( fn {name, arity} = fa -> warn( "cannot define @callback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition", @@ -811,7 +780,7 @@ defmodule Protocol do macrocallback_metas = callback_metas(env.module, :macrocallback) macrocallbacks = :maps.keys(macrocallback_metas) - :lists.map( + :lists.foreach( fn {name, arity} = fa -> warn( "cannot define @macrocallback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition", @@ -1002,14 +971,12 @@ defmodule Protocol do @doc false def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do - struct = Macro.struct!(for, env) - foreach = fn proto when is_atom(proto) -> - derive(proto, for, struct, [], env) + derive(proto, for, [], env) {proto, opts} when is_atom(proto) -> - derive(proto, for, struct, opts, env) + derive(proto, for, opts, env) end :lists.foreach(foreach, :lists.flatten(derives)) @@ -1017,21 +984,30 @@ defmodule Protocol do :ok end - defp derive(protocol, for, struct, opts, env) do + defp derive(protocol, for, opts, env) do extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}" assert_protocol!(protocol, extra) - __ensure_defimpl__(protocol, for, env) - assert_impl!(protocol, Any, extra) + + {mod, args} = + if macro_exported?(protocol, :__deriving__, 2) do + {protocol, [for, opts]} + else + # TODO: Deprecate this on Elixir v1.22+ + assert_impl!(protocol, Any, extra) + {Module.concat(protocol, Any), [for, Macro.struct!(for, env), opts]} + end # Clean up variables from eval context env = :elixir_env.reset_vars(env) - args = [for, struct, opts] - impl = Module.concat(protocol, Any) - :elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn mod, fun, args -> + :elixir_module.expand_callback(env.line, mod, :__deriving__, args, env, fn mod, fun, args -> if function_exported?(mod, fun, length(args)) do apply(mod, fun, args) else + __ensure_defimpl__(protocol, for, env) + assert_impl!(protocol, Any, extra) + impl = Module.concat(protocol, Any) + funs = for {fun, arity} <- protocol.__protocol__(:functions) do args = Macro.generate_arguments(arity, nil) diff --git a/lib/elixir/test/elixir/kernel/dialyzer_test.exs b/lib/elixir/test/elixir/kernel/dialyzer_test.exs index f78b214c0a..d055e71c61 100644 --- a/lib/elixir/test/elixir/kernel/dialyzer_test.exs +++ b/lib/elixir/test/elixir/kernel/dialyzer_test.exs @@ -41,6 +41,7 @@ defmodule Kernel.DialyzerTest do Macro, Macro.Env, Module, + Protocol, String, String.Chars ] diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index 7207c3b3e1..bae0550e3b 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -796,7 +796,7 @@ defmodule KernelTest do test ":struct" do assert Kernel.__info__(:struct) == nil - assert [%{field: :scheme, required: false, default: nil} | _] = URI.__info__(:struct) + assert [%{field: :scheme, default: nil} | _] = URI.__info__(:struct) end test "others" do diff --git a/lib/elixir/test/elixir/macro_test.exs b/lib/elixir/test/elixir/macro_test.exs index eba21a11fe..297e46f9f1 100644 --- a/lib/elixir/test/elixir/macro_test.exs +++ b/lib/elixir/test/elixir/macro_test.exs @@ -1552,28 +1552,28 @@ defmodule MacroTest do defstruct [:a, :b] assert Macro.struct_info!(StructBang, __ENV__) == [ - %{field: :a, required: false, default: nil}, - %{field: :b, required: false, default: nil} + %{field: :a, default: nil}, + %{field: :b, default: nil} ] def within_function do assert Macro.struct_info!(StructBang, __ENV__) == [ - %{field: :a, required: false, default: nil}, - %{field: :b, required: false, default: nil} + %{field: :a, default: nil}, + %{field: :b, default: nil} ] end defmodule Nested do assert Macro.struct_info!(StructBang, __ENV__) == [ - %{field: :a, required: false, default: nil}, - %{field: :b, required: false, default: nil} + %{field: :a, default: nil}, + %{field: :b, default: nil} ] end end assert Macro.struct_info!(StructBang, __ENV__) == [ - %{field: :a, required: false, default: nil}, - %{field: :b, required: false, default: nil} + %{field: :a, default: nil}, + %{field: :b, default: nil} ] end diff --git a/lib/elixir/test/elixir/protocol_test.exs b/lib/elixir/test/elixir/protocol_test.exs index 038721a7af..210f41b708 100644 --- a/lib/elixir/test/elixir/protocol_test.exs +++ b/lib/elixir/test/elixir/protocol_test.exs @@ -27,11 +27,11 @@ defmodule ProtocolTest do defprotocol Derivable do @undefined_impl_description "you should try harder" - def ok(a) - end - defimpl Derivable, for: Any do - defmacro __deriving__(module, struct, options) do + @impl true + defmacro __deriving__(module, options) do + struct = Macro.struct!(module, __CALLER__) + quote do defimpl Derivable, for: unquote(module) do def ok(arg) do @@ -41,6 +41,10 @@ defmodule ProtocolTest do end end + def ok(a) + end + + defimpl Derivable, for: Any do def ok(arg) do {:ok, arg} end diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 32f2afa5aa..5d31f6e224 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1106,8 +1106,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do File.write!("lib/a.ex", """ defmodule A do - @enforce_keys [:foo] - defstruct [:foo, :bar] + defstruct [:foo, bar: 1] end """) @@ -1118,8 +1117,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do File.write!("lib/a.ex", """ defmodule A do - @enforce_keys [:foo] - defstruct [:foo, :bar] + defstruct [:foo, bar: 1] def some_fun, do: :ok end """) @@ -1140,7 +1138,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do # At the code back and it should work again File.write!("lib/a.ex", """ defmodule A do - defstruct [:foo, :bar] + defstruct [:foo, bar: 1] end """)