Files
n8n-openai-adapter/lib/elixir/lib/protocol.ex
T

1224 lines
38 KiB
Elixir

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Protocol do
@moduledoc ~S"""
Reference and functions for working with protocols.
A protocol specifies an API that should be defined by its
implementations. A protocol is defined with `Kernel.defprotocol/2`
and its implementations with `Kernel.defimpl/3`.
## Example
In Elixir, we have two nouns for checking how many items there
are in a data structure: `length` and `size`. `length` means the
information must be computed. For example, `length(list)` needs to
traverse the whole list to calculate its length. On the other hand,
`tuple_size(tuple)` and `byte_size(binary)` do not depend on the
tuple and binary size as the size information is precomputed in
the data structure.
Although Elixir includes specific functions such as `tuple_size`,
`byte_size` and `map_size`, sometimes we want to be able to
retrieve the size of a data structure regardless of its type.
In Elixir we can write polymorphic code, i.e. code that works
with different shapes/types, by using protocols. A size protocol
could be implemented as follows:
defprotocol Size do
@doc "Calculates the size (and not the length!) of a data structure"
def size(data)
end
Now that the protocol can be implemented for every data structure
the protocol may have a compliant implementation for:
defimpl Size, for: BitString do
def size(binary), do: byte_size(binary)
end
defimpl Size, for: Map do
def size(map), do: map_size(map)
end
defimpl Size, for: Tuple do
def size(tuple), do: tuple_size(tuple)
end
Finally, we can use the `Size` protocol to call the correct implementation:
Size.size({1, 2})
# => 2
Size.size(%{key: :value})
# => 1
Note that we didn't implement it for lists as we don't have the
`size` information on lists, rather its value needs to be
computed with `length`.
The data structure you are implementing the protocol for
must be the first argument to all functions defined in the
protocol.
It is possible to implement protocols for all Elixir types:
* Structs (see the "Protocols and Structs" section below)
* `Tuple`
* `Atom`
* `List`
* `BitString`
* `Integer`
* `Float`
* `Function`
* `PID`
* `Map`
* `Port`
* `Reference`
* `Any` (see the "Fallback to `Any`" section below)
## Protocols and Structs
The real benefit of protocols comes when mixed with structs.
For instance, Elixir ships with many data types implemented as
structs, like `MapSet`. We can implement the `Size` protocol
for those types as well:
defimpl Size, for: MapSet do
def size(map_set), do: MapSet.size(map_set)
end
When implementing a protocol for a struct, the `:for` option can
be omitted if the `defimpl/3` call is inside the module that defines
the struct:
defmodule User do
defstruct [:email, :name]
defimpl Size do
# two fields
def size(%User{}), do: 2
end
end
If a protocol implementation is not found for a given type,
invoking the protocol will raise unless it is configured to
fall back to `Any`. Conveniences for building implementations
on top of existing ones are also available, look at `defstruct/1`
for more information about deriving protocols.
## Fallback to `Any`
In some cases, it may be convenient to provide a default
implementation for all types. This can be achieved by setting
the `@fallback_to_any` attribute to `true` in the protocol
definition:
defprotocol Size do
@fallback_to_any true
def size(data)
end
The `Size` protocol can now be implemented for `Any`:
defimpl Size, for: Any do
def size(_), do: 0
end
Although the implementation above is arguably not a reasonable
one. For example, it makes no sense to say a PID or an integer
have a size of `0`. That's one of the reasons why `@fallback_to_any`
is an opt-in behavior. For the majority of protocols, raising
an error when a protocol is not implemented is the proper behavior.
## Multiple implementations
Protocols can also be implemented for multiple types at once:
defprotocol Reversible do
def reverse(term)
end
defimpl Reversible, for: [Map, List] do
def reverse(term), do: Enum.reverse(term)
end
Inside `defimpl/3`, you can use `@protocol` to access the protocol
being implemented and `@for` to access the module it is being
defined for.
## Types
Defining a protocol automatically defines a zero-arity type named `t`, which
can be used as follows:
@spec print_size(Size.t()) :: :ok
def print_size(data) do
result =
case Size.size(data) do
0 -> "data has no items"
1 -> "data has one item"
n -> "data has #{n} items"
end
IO.puts(result)
end
The `@spec` above expresses that all types allowed to implement the
given protocol are valid argument types for the given function.
## Configuration
The following module attributes are available to configure a protocol:
* `@fallback_to_any` - when true, enables protocol dispatch to
fallback to any
* `@undefined_impl_description` - a string with additional description
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
## Consolidation
In order to speed up protocol dispatching, whenever all protocol implementations
are known up-front, typically after all Elixir code in a project is compiled,
Elixir provides a feature called *protocol consolidation*. Consolidation directly
links protocols to their implementations in a way that invoking a function from a
consolidated protocol is equivalent to invoking two remote functions - one to
identify the correct implementation, and another to call the implementation.
Protocol consolidation is applied by default to all Mix projects during compilation.
This may be an issue during test. For instance, if you want to implement a protocol
during test, the implementation will have no effect, as the protocol has already been
consolidated. One possible solution is to include compilation directories that are
specific to your test environment in your mix.exs:
def project do
...
elixirc_paths: elixirc_paths(Mix.env())
...
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
And then you can define the implementations specific to the test environment
inside `test/support/some_file.ex`.
Another approach is to disable protocol consolidation during tests in your
mix.exs:
def project do
...
consolidate_protocols: Mix.env() != :test
...
end
If you are using `Mix.install/2`, you can do by passing the `consolidate_protocols`
option:
Mix.install(
deps,
consolidate_protocols: false
)
Although doing so is not recommended as it may affect the performance of
your code.
Finally, note all protocols are compiled with `debug_info` set to `true`,
regardless of the option set by the `elixirc` compiler. The debug info is
used for consolidation and it is removed after consolidation unless
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
@elixir_checker_version :elixir_erl.checker_version()
@doc false
defmacro def(signature)
defmacro def({_, _, args}) when args == [] or is_atom(args) do
raise ArgumentError, "protocol functions expect at least one argument"
end
defmacro def({name, _, args}) when is_atom(name) and is_list(args) do
arity = length(args)
type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity))
type_args = [quote(do: t) | type_args]
to_var = fn pos -> Macro.var(String.to_atom("arg" <> Integer.to_string(pos)), __MODULE__) end
call_args = :lists.map(to_var, :lists.seq(2, arity))
call_args = [quote(do: term) | call_args]
quote generated: true do
name = unquote(name)
arity = unquote(arity)
@__functions__ [{name, arity} | @__functions__]
# Generate a fake definition with the user
# signature that will be used by docs
Kernel.def(unquote(name)(unquote_splicing(args)))
# Generate the actual implementation
Kernel.def unquote(name)(unquote_splicing(call_args)) do
impl_for!(term).unquote(name)(unquote_splicing(call_args))
end
# Copy spec as callback if possible,
# otherwise generate a dummy callback
Module.spec_to_callback(__MODULE__, {name, arity}) ||
@callback unquote(name)(unquote_splicing(type_args)) :: term
end
end
defmacro def(_) do
raise ArgumentError, "invalid arguments for def inside defprotocol"
end
@doc """
Checks if the given module is loaded and is protocol.
Returns `:ok` if so, otherwise raises `ArgumentError`.
"""
@spec assert_protocol!(module) :: :ok
def assert_protocol!(module) do
assert_protocol!(module, "")
end
defp assert_protocol!(module, extra) do
try do
Code.ensure_compiled!(module)
rescue
e in ArgumentError ->
raise ArgumentError, e.message <> extra
end
try do
module.__protocol__(:module)
rescue
UndefinedFunctionError ->
raise ArgumentError, "#{inspect(module)} is not a protocol" <> extra
end
:ok
end
@doc """
Checks if the given module is loaded and is an implementation
of the given protocol.
Returns `:ok` if so, otherwise raises `ArgumentError`.
"""
@spec assert_impl!(module, module) :: :ok
def assert_impl!(protocol, base) do
assert_impl!(protocol, base, "")
end
defp assert_impl!(protocol, base, extra) do
impl = __concat__(protocol, base)
try do
Code.ensure_compiled!(impl)
rescue
e in ArgumentError ->
raise ArgumentError, e.message <> extra
end
try do
impl.__impl__(:protocol)
rescue
UndefinedFunctionError ->
raise ArgumentError, "#{inspect(impl)} is not an implementation of a protocol" <> extra
else
^protocol ->
:ok
other ->
raise ArgumentError,
"expected #{inspect(impl)} to be an implementation of #{inspect(protocol)}" <>
", got: #{inspect(other)}" <> extra
end
end
@doc """
Derives the `protocol` for `module` with the given options.
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
@impl true
defmacro __deriving__(module, options) do
# If you need to load struct metadata, you may call:
# struct_info = Macro.struct_info!(module, __CALLER__)
quote do
defimpl Derivable, for: unquote(module) do
def ok(arg) do
{:ok, arg, unquote(options)}
end
end
end
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}, []}
If the struct has already been defined, you can call this macro:
require Protocol
Protocol.derive(Derivable, ImplStruct, :oops)
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, :oops}
"""
defmacro derive(protocol, module, options \\ []) do
quote do
Protocol.__derive__([{unquote(protocol), unquote(options)}], unquote(module), __ENV__)
end
end
## Consolidation
@doc """
Extracts all protocols from the given paths.
The paths can be either a charlist or a string. Internally
they are worked on as charlists, so passing them as lists
avoid extra conversion.
Does not load any of the protocols.
## Examples
# Get Elixir's ebin directory path and retrieve all protocols
iex> path = Application.app_dir(:elixir, "ebin")
iex> mods = Protocol.extract_protocols([path])
iex> Enumerable in mods
true
"""
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_protocols(paths) do
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
case attributes[:__protocol__] do
[fallback_to_any: _] -> module
_ -> nil
end
end)
end
@doc """
Extracts all types implemented for the given protocol from
the given paths.
The paths can be either a charlist or a string. Internally
they are worked on as charlists, so passing them as lists
avoid extra conversion.
Does not load any of the implementations.
## Examples
# Get Elixir's ebin directory path and retrieve all protocols
iex> path = Application.app_dir(:elixir, "ebin")
iex> mods = Protocol.extract_impls(Enumerable, [path])
iex> List in mods
true
"""
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
def extract_impls(protocol, paths) when is_atom(protocol) do
prefix = Atom.to_charlist(protocol) ++ [?.]
extract_matching_by_attribute(paths, prefix, fn _mod, attributes ->
case attributes[:__impl__] do
[protocol: ^protocol, for: for] -> for
_ -> nil
end
end)
end
defp extract_matching_by_attribute(paths, prefix, callback) do
for path <- paths,
{path, files} = list_dir(path),
file <- files,
mod = extract_from_file(path, file, prefix, callback),
do: mod
end
# Do not use protocols as they may be consolidating
defp list_dir({path, files}) when is_list(path) and is_list(files) do
{path, files}
end
defp list_dir(path) when is_binary(path) do
list_dir(String.to_charlist(path))
end
defp list_dir(path) when is_list(path) do
case :file.list_dir(path) do
{:ok, files} -> {path, files}
_ -> {path, []}
end
end
defp extract_from_file(path, file, prefix, callback) do
if :lists.prefix(prefix, file) and :filename.extension(file) == [?., ?b, ?e, ?a, ?m] do
extract_from_beam(:filename.join(path, file), callback)
end
end
defp extract_from_beam(file, callback) do
case :beam_lib.chunks(file, [:attributes]) do
{:ok, {module, [attributes: attributes]}} ->
callback.(module, attributes)
_ ->
nil
end
end
@doc """
Returns `true` if the protocol was consolidated.
"""
@spec consolidated?(module) :: boolean
def consolidated?(protocol) do
protocol.__protocol__(:consolidated?)
end
@doc """
Receives a protocol and a list of implementations and
consolidates the given protocol.
Consolidation happens by changing the protocol `impl_for`
in the abstract format to have fast lookup rules. Usually
the list of implementations to use during consolidation
are retrieved with the help of `extract_impls/2`.
It returns the updated version of the protocol bytecode.
If the first element of the tuple is `:ok`, it means
the protocol was consolidated.
A given bytecode or protocol implementation can be checked
to be consolidated or not by analyzing the protocol
attribute:
Protocol.consolidated?(Enumerable)
This function does not load the protocol at any point
nor loads the new bytecode for the compiled module.
However, each implementation must be available and
it will be loaded.
"""
@spec consolidate(module, [module]) ::
{:ok, binary}
| {:error, :not_a_protocol}
| {:error, :no_beam_info}
def consolidate(protocol, types) when is_atom(protocol) do
# Ensure the types are sorted so the compiled beam is deterministic
types = Enum.sort(types)
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
{:ok, definitions, checker} <-
consolidate(protocol, any, definitions, checker, types),
do: compile(definitions, checker, compile_info)
end
defp beam_protocol(protocol) do
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
opts = [:allow_missing_chunks]
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
%{attributes: attributes, definitions: definitions} = module_map
case attributes[:__protocol__] do
[fallback_to_any: any] ->
checker =
with true <- is_binary(checker),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
contents
else
_ -> nil
end
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
{:ok, any, definitions, checker, {module_map, specs, chunks}}
_ ->
{:error, :not_a_protocol}
end
_ ->
{:error, :no_beam_info}
end
end
defp beam_file(module) when is_atom(module) do
case :code.which(module) do
[_ | _] = file -> file
_ -> module
end
end
# Consolidate the protocol for faster implementations and fine-grained type information.
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
case List.keytake(definitions, {:__protocol__, 1}, 0) do
{protocol_def, definitions} ->
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
built_in_plus_any = [Any] ++ for {mod, _guard} <- built_in(), do: mod
structs = types -- built_in_plus_any
{impl_for, definitions} = List.keytake(definitions, {:impl_for, 1}, 0)
{impl_for!, definitions} = List.keytake(definitions, {:impl_for!, 1}, 0)
{struct_impl_for, definitions} = List.keytake(definitions, {:struct_impl_for, 1}, 0)
protocol_funs = get_protocol_functions(protocol_def)
protocol_def = change_protocol(protocol_def, types)
impl_for = change_impl_for(impl_for, protocol, types)
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
checker =
if checker do
update_in(checker.exports, fn exports ->
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
for {fun, info} <- exports do
if sig = Map.get(signatures, fun) do
{fun, %{info | sig: sig}}
else
{fun, info}
end
end
end)
end
{:ok, definitions, checker}
nil ->
{:error, :not_a_protocol}
end
end
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
alias Module.Types.Descr
types_minus_any = List.delete(types, Any)
clauses =
Enum.map(types_minus_any, fn impl ->
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
end)
{domain, impl_for, impl_for!} =
case clauses do
[] ->
if Any in types do
clauses = [{[Descr.term()], Descr.atom([__concat__(protocol, Any)])}]
{Descr.term(), clauses, clauses}
else
{Descr.none(), [{[Descr.term()], Descr.atom([nil])}],
[{[Descr.none()], Descr.none()}]}
end
_ ->
structs_domain =
case structs do
[] -> Descr.none()
_ -> Descr.open_map(__struct__: Descr.atom(structs))
end
domain =
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
Descr.union(Module.Types.Of.impl(impl, :open), acc)
end)
not_domain = Descr.negation(domain)
if Any in types do
clauses =
clauses ++ [{[not_domain], Descr.atom([__concat__(protocol, Any)])}]
{Descr.term(), clauses, clauses}
else
{domain, clauses ++ [{[not_domain], Descr.atom([nil])}], clauses}
end
end
new_signatures =
for {{_fun, arity} = fun_arity, :def, _, _} <- definitions,
fun_arity in protocol_funs do
rest = List.duplicate(Descr.term(), arity - 1)
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
end
Map.new(
[
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
] ++ new_signatures
)
end
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
Enum.find_value(clauses, fn
{_meta, [:functions], [], clauses} -> clauses
_ -> nil
end) || raise "could not find protocol functions"
end
defp change_protocol({_name, _kind, meta, clauses}, types) do
clauses =
Enum.map(clauses, fn
{meta, [:consolidated?], [], _} -> {meta, [:consolidated?], [], true}
{meta, [:impls], [], _} -> {meta, [:impls], [], {:consolidated, types}}
clause -> clause
end)
{{:__protocol__, 1}, :def, meta, clauses}
end
defp change_impl_for({_name, _kind, meta, _clauses}, protocol, types) do
fallback = if Any in types, do: __concat__(protocol, Any)
line = meta[:line]
clauses =
for {mod, guard} <- built_in(),
mod in types,
do: built_in_clause_for(mod, guard, protocol, meta, line)
struct_clause = struct_clause_for(meta, line)
fallback_clause = fallback_clause_for(fallback, protocol, meta)
clauses = [struct_clause] ++ clauses ++ [fallback_clause]
{{:impl_for, 1}, :def, meta, clauses}
end
defp change_struct_impl_for({_name, _kind, meta, _clauses}, protocol, types, structs) do
fallback = if Any in types, do: __concat__(protocol, Any)
clauses = for struct <- structs, do: each_struct_clause_for(struct, protocol, meta)
clauses = clauses ++ [fallback_clause_for(fallback, protocol, meta)]
{{:struct_impl_for, 1}, :defp, meta, clauses}
end
defp built_in_clause_for(mod, guard, protocol, meta, line) do
x = {:x, [line: line, version: -1], __MODULE__}
guard = quote(line: line, do: :erlang.unquote(guard)(unquote(x)))
body = __concat__(protocol, mod)
{meta, [x], [guard], body}
end
defp struct_clause_for(meta, line) do
x = {:x, [line: line, version: -1], __MODULE__}
head = quote(line: line, do: %{__struct__: unquote(x)})
guard = quote(line: line, do: :erlang.is_atom(unquote(x)))
body = quote(line: line, do: struct_impl_for(unquote(x)))
{meta, [head], [guard], body}
end
defp each_struct_clause_for(struct, protocol, meta) do
{meta, [struct], [], __concat__(protocol, struct)}
end
defp fallback_clause_for(value, _protocol, meta) do
{meta, [quote(do: _)], [], value}
end
# Finally compile the module and emit its bytecode.
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
module_map = %{module_map | definitions: definitions}
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
end
## Definition callbacks
@doc false
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]
# Import the new `def` that is used by protocols
import Protocol, only: [def: 1]
# Compile with debug info for consolidation
@compile :debug_info
# Set up a clear slate to store defined functions
@__functions__ []
@fallback_to_any false
# Invoke the user given block
_ = unquote(block)
# Finalize expansion
unquote(after_defprotocol())
end
end
end
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}
) do
[{{name, length(List.wrap(args))}, meta}]
end
defp callback_ast_to_fa({_kind, _clause, _pos}) do
[]
end
defp callback_metas(module, kind)
when kind in [:callback, :macrocallback] do
:lists.flatmap(&callback_ast_to_fa/1, Module.get_attribute(module, kind))
|> :maps.from_list()
end
defp get_callback_line(fa, metas),
do: :maps.get(fa, metas, [])[:line]
defp warn(message, env, nil) do
IO.warn(message, env)
end
defp warn(message, env, line) when is_integer(line) do
IO.warn(message, %{env | line: line})
end
def __before_compile__(env) do
functions = Module.get_attribute(env.module, :__functions__)
if functions == [] do
warn(
"protocols must define at least one function, but none was defined",
env,
nil
)
end
extra =
((Module.definitions_in(env.module, :def) ++ Module.definitions_in(env.module, :defmacro)) --
functions) --
[impl_for: 1, impl_for!: 1, __protocol__: 1, __deriving__: 2, __deriving__: 3]
# TODO: Make an error on Elixir v2.0
if extra != [] do
warn(
"protocols can only define functions without implementation via def/1, found: " <>
Enum.map_join(extra, ", ", fn {name, arity} -> "#{name}/#{arity}" end),
env,
nil
)
end
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
# TODO: Make an error on Elixir v2.0
:lists.foreach(
fn {name, arity} = fa ->
warn(
"cannot define @callback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
env,
get_callback_line(fa, callback_metas)
)
end,
callbacks -- functions
)
# Macro Callbacks
macrocallback_metas = callback_metas(env.module, :macrocallback)
macrocallbacks = :maps.keys(macrocallback_metas)
# TODO: Make an error on Elixir v2.0
:lists.foreach(
fn {name, arity} = fa ->
warn(
"cannot define @macrocallback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
env,
get_callback_line(fa, macrocallback_metas)
)
end,
macrocallbacks
)
# Optional Callbacks
optional_callbacks = Module.get_attribute(env.module, :optional_callbacks)
if optional_callbacks != [] do
warn(
"cannot define @optional_callbacks inside protocol, all of the protocol definitions are required",
env,
nil
)
end
end
defp after_defprotocol do
quote bind_quoted: [built_in: built_in()] do
any_impl_for =
if @fallback_to_any do
__MODULE__.Any
else
nil
end
# Disable Dialyzer checks - before and after consolidation
# the types could be more strict
@dialyzer {:nowarn_function, __protocol__: 1, impl_for: 1, impl_for!: 1}
@doc false
@spec impl_for(term) :: atom | nil
Kernel.def(impl_for(data))
# Define the implementation for structs.
#
# It simply delegates to struct_impl_for which is then
# optimized during protocol consolidation.
Kernel.def impl_for(%struct{}) do
struct_impl_for(struct)
end
# Define the implementation for built-ins
:lists.foreach(
fn {mod, guard} ->
target = Protocol.__concat__(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
end,
built_in
)
# Define a catch-all impl_for/1 clause to pacify Dialyzer (since
# destructuring opaque types is illegal, Dialyzer will think none of the
# previous clauses matches opaque types, and without this clause, will
# conclude that impl_for can't handle an opaque argument). This is a hack
# since it relies on Dialyzer not being smart enough to conclude that all
# opaque types will get the any_impl_for/0 implementation.
Kernel.def impl_for(_) do
unquote(any_impl_for)
end
undefined_impl_description =
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
@doc false
@spec impl_for!(term) :: atom
if any_impl_for do
Kernel.def impl_for!(data) do
impl_for(data)
end
else
Kernel.def impl_for!(data) do
impl_for(data) ||
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: unquote(undefined_impl_description)
)
end
end
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
# Inline struct implementation for performance
@compile {:inline, struct_impl_for: 1}
if not Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@type t :: term
end
# Store information as an attribute so it
# can be read without loading the module.
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
@__protocol__ [fallback_to_any: !!@fallback_to_any]
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: [{atom(), arity()}]
@spec __protocol__(:consolidated?) :: boolean()
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
Kernel.def(__protocol__(:module), do: __MODULE__)
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
Kernel.def(__protocol__(:consolidated?), do: false)
Kernel.def(__protocol__(:impls), do: :not_consolidated)
end
end
@doc false
def __impl__(protocol, opts, do_block, env) do
opts = Keyword.merge(opts, do_block)
{for, opts} =
Keyword.pop_lazy(opts, :for, fn ->
env.module ||
raise ArgumentError, "defimpl/3 expects a :for option when declared outside a module"
end)
expansion_env = %{env | module: env.module || Elixir, function: {:__impl__, 1}}
protocol = Macro.expand_literals(protocol, expansion_env)
for = Macro.expand_literals(for, expansion_env)
case opts do
[] -> raise ArgumentError, "defimpl expects a do-end block"
[do: block] -> __impl__(protocol, for, block)
_ -> raise ArgumentError, "unknown options given to defimpl, got: #{Macro.to_string(opts)}"
end
end
defp __impl__(protocol, for, block) when is_list(for) do
for f <- for, do: __impl__(protocol, f, block)
end
defp __impl__(protocol, for, block) do
# Unquote the implementation just later
# when all variables will already be injected
# into the module body.
impl =
quote unquote: false do
@doc false
@spec __impl__(:for) :: unquote(for)
@spec __impl__(:protocol) :: unquote(protocol)
def __impl__(:for), do: unquote(for)
def __impl__(:protocol), do: unquote(protocol)
end
# If the protocol is an atom, we will add an export dependency,
# since it was expanded before-hand. Otherwise it is a dynamic
# expression (and therefore most likely a compile-time one).
behaviour =
if is_atom(protocol) do
quote(do: require(unquote(protocol)))
else
quote(do: protocol)
end
quote do
protocol = unquote(protocol)
for = unquote(for)
name = Protocol.__concat__(protocol, for)
Protocol.assert_protocol!(protocol)
Protocol.__impl__!(protocol, for, __ENV__)
defmodule name do
@moduledoc false
@behaviour unquote(behaviour)
@protocol protocol
@for for
res = unquote(block)
Module.register_attribute(__MODULE__, :__impl__, persist: true)
@__impl__ [protocol: @protocol, for: @for]
unquote(impl)
res
end
end
end
@doc false
def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
foreach = fn
proto when is_atom(proto) ->
derive(proto, for, [], env)
{proto, opts} when is_atom(proto) ->
derive(proto, for, opts, env)
end
:lists.foreach(foreach, :lists.flatten(derives))
:ok
end
defp derive(protocol, for, opts, env) do
extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
assert_protocol!(protocol, 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)
{__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]}
end
# Clean up variables from eval context
env = :elixir_env.reset_vars(env)
: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
__impl__!(protocol, for, env)
assert_impl!(protocol, Any, extra)
impl = __concat__(protocol, Any)
funs =
for {fun, arity} <- protocol.__protocol__(:functions) do
args = Macro.generate_arguments(arity, nil)
quote do
def unquote(fun)(unquote_splicing(args)),
do: unquote(impl).unquote(fun)(unquote_splicing(args))
end
end
quoted =
quote do
@behaviour unquote(protocol)
Module.register_attribute(__MODULE__, :__impl__, persist: true)
@__impl__ [protocol: unquote(protocol), for: unquote(for)]
@doc false
@spec __impl__(:protocol) :: unquote(protocol)
@spec __impl__(:for) :: unquote(for)
def __impl__(:protocol), do: unquote(protocol)
def __impl__(:for), do: unquote(for)
end
Module.create(__concat__(protocol, for), [quoted | funs], Macro.Env.location(env))
end
end)
end
@doc false
def __impl__!(protocol, for, env) do
if not Code.get_compiler_option(:ignore_already_consolidated) and
Protocol.consolidated?(protocol) do
message =
"the #{inspect(protocol)} protocol has already been consolidated, an " <>
"implementation for #{inspect(for)} has no effect. If you want to " <>
"implement protocols after compilation or during tests, check the " <>
"\"Consolidation\" section in the Protocol module documentation"
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 Code.ensure_compiled(for) != {:module, 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
defp built_in do
[
{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
@doc false
def __concat__(left, right) do
String.to_atom(
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
)
end
defp ensure_prefix("Elixir." <> _ = left), do: left
defp ensure_prefix(left), do: "Elixir." <> left
defp remove_prefix("Elixir." <> right), do: right
defp remove_prefix(right), do: right
end