Skip to content

Commit

Permalink
Move __deriving__ callback to the protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Nov 27, 2024
1 parent 1f98708 commit e3bad83
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 208 deletions.
152 changes: 79 additions & 73 deletions lib/elixir/lib/inspect.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)}
Expand Down
12 changes: 6 additions & 6 deletions lib/elixir/lib/kernel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/elixir/lib/kernel/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
Expand Down
3 changes: 2 additions & 1 deletion lib/elixir/lib/module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion lib/elixir/lib/module/types/apply.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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__: [
Expand Down
Loading

0 comments on commit e3bad83

Please sign in to comment.