Current section

Files

Jump to
bond lib bond compiler protocol_wrapper.ex
Raw

lib/bond/compiler/protocol_wrapper.ex

defmodule Bond.Compiler.ProtocolWrapper do
@moduledoc internal: true
@moduledoc """
Generates protocol-level contract code: the dispatch-layer wrapper (for the protocol module)
and the effective-contract functions (for impl modules that opt in via `Bond.Protocol.Impl`).
All generated code uses fully-qualified `Kernel.def`/`Kernel.defp`/`Kernel.defoverridable`
because the protocol body excludes `Kernel.def`. The lifted assertion bodies reuse
`Bond.Compiler.Assertion.assertions_body/3` (so the conditional-compilation chain, error
structs, and stacktrace pruning are shared with ordinary contracts), and the runtime gate uses
a compile default of `true` — global config and `Bond.Config` still toggle protocol contracts
at runtime via `Bond.Runtime.Eval.should_evaluate?/3`.
See `Bond.Protocol` for the "Option B" design (wrap the single dispatch function, once, at
`@before_compile`).
## Refinement-aware dispatch (Phase B)
The generated wrapper resolves the implementation module once (via `Protocol.impl_for!/1`)
and uses that resolved module to replace the former `super(...)` call. This single resolution
is also used to check for per-implementation refinements at runtime: if an impl module exports
`__bond_effective_pre__<name>_<arity>__` or `__bond_effective_post__<name>_<arity>__`
(generated by `build_effective_fns/9`), those are called instead of the protocol's own lifted
assertion defps. Non-refining impls are completely unaffected. The naming convention for those
functions is canonical here: `eff_pre_fn/2` and `eff_post_fn/2` are the single source of truth.
"""
alias Bond.Compiler.Assertion
@doc """
Returns the atom name of the effective-precondition function that `build_effective_fns/9`
generates on a refining impl module, and that the dispatch wrapper checks for at runtime.
"""
def eff_pre_fn(name, arity), do: :"__bond_effective_pre__#{name}_#{arity}__"
@doc """
Returns the atom name of the effective-postcondition function that `build_effective_fns/9`
generates on a refining impl module, and that the dispatch wrapper checks for at runtime.
"""
def eff_post_fn(name, arity), do: :"__bond_effective_post__#{name}_#{arity}__"
@doc """
Builds the `{:__block__, _, _}` of statements that wrap one protocol function: the
`defoverridable`, the redefined dispatch `def` that checks pre/super/post, and the lifted
precondition/postcondition `defp`s.
"""
def build_wrapper(protocol, name, arity, arg_names, pre, post) do
arg_vars = Enum.map(arg_names, &Macro.var(&1, nil))
result_var = Macro.var(:result, nil)
impl_var = Macro.var(:bond_impl, __MODULE__)
subject = List.first(arg_vars)
function_info = {name, arity}
pre_fn = :"__bond_protocol_pre_#{name}_#{arity}__"
post_fn = :"__bond_protocol_post_#{name}_#{arity}__"
eff_pre_fn = eff_pre_fn(name, arity)
eff_post_fn = eff_post_fn(name, arity)
pre_chain = if pre != [], do: true, else: :purge
# Resolve the implementation module once, then call it directly (avoids double impl_for!
# that would occur if we kept `super(args)` alongside the resolution for the refinement check).
impl_resolution =
quote do
unquote(impl_var) = unquote(protocol).impl_for!(unquote(subject))
end
# Direct call to the impl module, replacing the former super(args).
impl_call_ast = {{:., [], [impl_var, name]}, [], arg_vars}
# Post is always present (even when post == []) so that impls with @post_strengthen
# (which adds a postcondition where the protocol declared none) are still enforced.
wrapper_body =
[impl_resolution] ++
if(pre != [],
do: [pre_eval_stmt(protocol, subject, pre_fn, eff_pre_fn, arity, arg_vars, impl_var)],
else: []
) ++
[quote(do: unquote(result_var) = unquote(impl_call_ast))] ++
[
post_eval_stmt(
protocol,
subject,
post_fn,
eff_post_fn,
arity + 1,
arg_vars,
impl_var,
result_var,
pre_chain,
post != []
)
] ++
[result_var]
statements =
[
quote do
Kernel.defoverridable([{unquote(name), unquote(arity)}])
Kernel.def unquote(name)(unquote_splicing(arg_vars)) do
(unquote_splicing(wrapper_body))
end
end
] ++
if pre != [] do
[
quote do
Kernel.defp unquote(pre_fn)(unquote_splicing(arg_vars)) do
unquote(Assertion.assertions_body(pre, function_info))
end
end
]
else
[]
end ++
if post != [] do
[
quote do
Kernel.defp unquote(post_fn)(unquote_splicing(arg_vars), unquote(result_var)) do
unquote(Assertion.assertions_body(post, function_info))
end
end
]
else
[]
end
{:__block__, [], statements}
end
@doc """
Builds the quoted `def`s emitted on a refining impl module by `Bond.Protocol.Impl`:
`__bond_effective_pre__<name>_<arity>__` (when `pre_weaken` is non-empty) and
`__bond_effective_post__<name>_<arity>__` (when `post_strengthen` is non-empty).
The caller (`Bond.Protocol.Impl.__before_compile__`) is responsible for all validation
(fetching the protocol contract, checking that an inherited pre exists before weakening,
validating referenced names). This function only generates AST.
"""
def build_effective_fns(
name,
arity,
canonical_arg_names,
inherited_pre,
inherited_post,
pre_weaken,
post_strengthen,
function_module,
env
) do
function_info = {name, arity}
arg_vars = Enum.map(canonical_arg_names, &Macro.var(&1, nil))
result_var = Macro.var(:result, nil)
eff_pre = eff_pre_fn(name, arity)
eff_post = eff_post_fn(name, arity)
pre_stmt =
if pre_weaken != [] do
body =
Assertion.pre_weaken_body(inherited_pre, pre_weaken, function_info, function_module)
quote file: env.file, line: env.line do
@dialyzer {:nowarn_function, [{unquote(eff_pre), unquote(arity)}]}
@doc false
def unquote(eff_pre)(unquote_splicing(arg_vars)) do
unquote(body)
end
end
end
post_stmt =
if post_strengthen != [] do
body =
Assertion.post_strengthen_body(
inherited_post,
post_strengthen,
function_info,
function_module
)
quote file: env.file, line: env.line do
@dialyzer {:nowarn_function, [{unquote(eff_post), unquote(arity + 1)}]}
@doc false
def unquote(eff_post)(unquote_splicing(arg_vars), unquote(result_var)) do
unquote(body)
end
end
end
[pre_stmt, post_stmt] |> Enum.reject(&is_nil/1)
end
# Pre eval stmt: checks function_exported? on the resolved impl module and uses the impl's
# effective-pre function when present (refinement path); falls through to the protocol's
# own lifted pre defp otherwise (standard path).
defp pre_eval_stmt(protocol, subject, pre_fn, eff_pre_fn, eff_arity, arg_vars, impl_var) do
eff_pre_call = {{:., [], [impl_var, eff_pre_fn]}, [], arg_vars}
pre_fn_call = quote do: unquote(pre_fn)(unquote_splicing(arg_vars))
quote do
if Bond.Runtime.Eval.should_evaluate?(:preconditions, true) do
Bond.Runtime.Eval.evaluate_protocol_assertions(unquote(protocol), unquote(subject), fn ->
if :erlang.function_exported(
unquote(impl_var),
unquote(eff_pre_fn),
unquote(eff_arity)
) do
unquote(eff_pre_call)
else
unquote(pre_fn_call)
end
end)
end
end
end
# Post eval stmt: always emitted (even when post == []) so that impls with @post_strengthen
# are enforced even when the protocol itself declared no postcondition. The `has_verbatim_post?`
# flag controls whether the else branch calls the protocol's post defp or returns :ok.
defp post_eval_stmt(
protocol,
subject,
post_fn,
eff_post_fn,
eff_post_arity,
arg_vars,
impl_var,
result_var,
pre_chain,
has_verbatim_post?
) do
chain = Macro.escape(%{preconditions: pre_chain})
eff_post_call = {{:., [], [impl_var, eff_post_fn]}, [], arg_vars ++ [result_var]}
fallback =
if has_verbatim_post? do
quote do: unquote(post_fn)(unquote_splicing(arg_vars), unquote(result_var))
else
quote do: :ok
end
quote do
if Bond.Runtime.Eval.should_evaluate?(:postconditions, true, unquote(chain)) do
Bond.Runtime.Eval.evaluate_protocol_assertions(unquote(protocol), unquote(subject), fn ->
if :erlang.function_exported(
unquote(impl_var),
unquote(eff_post_fn),
unquote(eff_post_arity)
) do
unquote(eff_post_call)
else
unquote(fallback)
end
end)
end
end
end
end