Packages
bond
0.17.0
1.13.0
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.0
1.4.0
1.3.0
1.3.0-rc.1
1.2.1
1.2.0
1.1.0
1.0.0
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.1.0
Design by Contract (DbC) for Elixir
Current section
Files
Jump to
Current section
Files
lib/bond/runtime/eval.ex
defmodule Bond.Runtime.Eval do
@moduledoc internal: true
require Logger
@moduledoc """
Internal helper module for runtime execution of contracts and assertions.
The runtime evaluation of a contract is split into two halves so that the work of building
the assertion-evaluation closure can be avoided when the runtime guard says skip:
1. `should_evaluate?/3` — read `Application.get_env(:bond, kind, compile_default)` and
return `false` when the value is exactly `false`. Also enforces the contract-checking
chain (`preconditions ≤ postconditions ≤ invariants`): if a lower kind is runtime-off,
the higher kind is also skipped, with a one-time `Logger.warning` per process per
(higher, lower) pair. Callers (the override clauses generated by
`Bond.Compiler.AnnotatedFunction`/`Bond.Compiler.Invariants` and the `Bond.check/1,2`
macro expansion) wrap the assertion-evaluation call in this check so the closure
capturing the call site's bindings is allocated only when we're actually going to
evaluate.
2. `evaluate_preconditions/1` / `evaluate_postconditions/1` / `evaluate_invariants/1` /
`evaluate_check/1` — enforce the Assertion Evaluation rule via a process-dict
recursion guard, invoke the supplied 0-arity fn, and on `{:assertion_failure, info}`
throw, raise the appropriate `Bond.PreconditionError` / `Bond.PostconditionError` /
`Bond.InvariantError` / `Bond.CheckError`.
Centralising the runtime policy here (both the "decide" and "act" halves) keeps all
runtime contract behaviour in the `Bond.Runtime` namespace and minimises the code injected
into using modules.
"""
@assertion_errors %{
precondition: Bond.PreconditionError,
postcondition: Bond.PostconditionError,
check: Bond.CheckError,
invariant: Bond.InvariantError
}
@type assertion_fun :: (() -> term())
@type compile_default :: boolean()
@type kind :: :preconditions | :postconditions | :checks | :invariants
@doc """
Returns `true` when assertions of `kind` should be evaluated at this call site.
Reads `Application.get_env(:bond, kind, compile_default)` and returns `false` only when
the result is exactly `false`. `compile_default` is the per-module compile-time-resolved
mode for the kind, baked into the generated call site.
The third argument carries the compile-time defaults of every lower kind in the
contract-checking chain (`preconditions ≤ postconditions ≤ invariants`). When any of
those lower kinds is `false` at runtime, the higher kind is also skipped — propagation
ensures a `:postconditions` failure isn't reported when `:preconditions` weren't even
checked. The first time propagation fires for a (higher, lower) pair within a process,
Bond emits a `Logger.warning` describing the skip.
For `:preconditions` (bottom of the chain) the map is empty; for `:postconditions` it
carries `:preconditions`; for `:invariants` it carries both. The `:checks` kind is
independent of the chain — its callers pass `%{}`.
"""
@spec should_evaluate?(kind(), compile_default(), %{optional(kind()) => compile_default()}) ::
boolean()
def should_evaluate?(kind, compile_default, chain_defaults \\ %{}) do
if runtime_enabled?(kind, compile_default) do
chain_clear?(kind, chain_defaults)
else
false
end
end
defp runtime_enabled?(kind, compile_default) do
Application.get_env(:bond, kind, compile_default) != false
end
# Returns `true` when every lower-chain kind is runtime-on. Returns `false` as soon as
# one is `false` at runtime — that's the propagation. The `higher_kind` is the kind
# that *wanted* to run; we pass it through so the propagation log can attribute the
# skip to a (higher, lower) pair. `chain_defaults` carries each lower kind's
# compile-time default; `Application.get_env/3` falls back to that so per-module
# settings are honoured.
defp chain_clear?(higher_kind, chain_defaults) do
Enum.reduce_while(chain_defaults, true, fn {lower_kind, lower_default}, _acc ->
if runtime_enabled?(lower_kind, lower_default) do
{:cont, true}
else
maybe_log_propagation(higher_kind, lower_kind)
{:halt, false}
end
end)
end
# When propagation causes a higher kind to be skipped because a lower one is off,
# emit a one-time-per-process Logger warning. The dedup key is (higher, lower) per
# process — so a long-running OTP process logs at most one message per pair, but
# different processes (or processes restarted by their supervisors) get their own
# warnings. Tests reset the marker by running in fresh processes anyway.
defp maybe_log_propagation(higher_kind, lower_kind) do
key = {:__bond_propagation_logged__, higher_kind, lower_kind}
if Process.get(key) != true do
Process.put(key, true)
Logger.warning("""
Bond: :#{higher_kind} skipped because :bond, :#{lower_kind} is `false` at runtime.
The chain `preconditions ≤ postconditions ≤ invariants` requires that lower kinds be
enabled whenever higher kinds are. A :#{higher_kind} check is only meaningful if
:#{lower_kind} held first; with :#{lower_kind} disabled, :#{higher_kind} is also
skipped to avoid misleading errors.
To re-enable :#{higher_kind} evaluation, also enable :#{lower_kind}:
Application.put_env(:bond, :#{lower_kind}, true)
This warning is logged once per process per (higher, lower) pair.
""")
end
end
@spec evaluate_preconditions(assertion_fun()) :: term()
def evaluate_preconditions(preconditions_fun) when is_function(preconditions_fun, 0) do
evaluate_assertions(preconditions_fun)
end
@spec evaluate_postconditions(assertion_fun()) :: term()
def evaluate_postconditions(postconditions_fun) when is_function(postconditions_fun, 0) do
evaluate_assertions(postconditions_fun)
end
@spec evaluate_check(assertion_fun()) :: term()
def evaluate_check(check_fun) when is_function(check_fun, 0) do
evaluate_assertions(check_fun)
end
@spec evaluate_invariants(assertion_fun()) :: term()
def evaluate_invariants(invariants_fun) when is_function(invariants_fun, 0) do
evaluate_assertions(invariants_fun)
end
defp evaluate_assertions(assertions_fun) do
try do
with_recursion_check(assertions_fun)
catch
{:assertion_failure, %{kind: kind} = assertion_info} ->
emit_failure_event(assertion_info)
exception_module = Map.fetch!(@assertion_errors, kind)
exception = exception_module.exception(assertion_info)
# Strip Bond internal frames from the stacktrace so the failure points at the user's
# call site rather than into Bond.Runtime.Eval or the generated __bond_* defps.
:erlang.raise(:error, exception, prune_stacktrace(__STACKTRACE__))
end
end
# Fire `[:bond, :assertion, :failure]` whenever an assertion is violated. Pass the full
# `assertion_info` map as metadata so consumers can filter on `:kind`, attribute failures to
# a specific `:assertion_id`, or inspect the captured `:binding`. Measurements carry
# `:system_time` and `:monotonic_time` so consumers can use the event as a time-series
# signal without an additional clock read.
defp emit_failure_event(assertion_info) do
measurements = %{
system_time: System.system_time(),
monotonic_time: System.monotonic_time()
}
:telemetry.execute([:bond, :assertion, :failure], measurements, assertion_info)
end
defp prune_stacktrace(stacktrace) do
Enum.reject(stacktrace, &bond_frame?/1)
end
defp bond_frame?({module, fun, _arity_or_args, _location})
when is_atom(module) and is_atom(fun) do
bond_module?(module) or bond_generated_function?(fun)
end
defp bond_frame?(_), do: false
defp bond_module?(module) do
case Atom.to_string(module) do
"Elixir.Bond" -> true
"Elixir.Bond." <> _ -> true
_ -> false
end
end
# The lifted-closure defps emitted by `Bond.Compiler.AnnotatedFunction` live in the user's
# module but are Bond-generated. Hide them from stacktraces so failures point at the user's
# call site, consistent with the existing Bond.* frame pruning.
defp bond_generated_function?(fun) do
case Atom.to_string(fun) do
"__bond_" <> _ -> true
_ -> false
end
end
defp with_recursion_check(assertions_fun) do
# Mutually recursive contracts lead to infinite recursion, so don't evaluate assertions for a
# function if other assertions are already being evaluated in the current process.
#
# Assertion Evaluation rule (from Object-Oriented Software Construction):
# During the process of evaluating an assertion at run-time, routine calls shall
# be executed without any evaluation of the associated assertions.
if not evaluating_assertions?() do
set_evaluating_assertions(true)
try do
assertions_fun.()
after
set_evaluating_assertions(false)
end
end
end
defp evaluating_assertions? do
Process.get(:__bond_evaluating_assertions__, false)
end
defp set_evaluating_assertions(bool) do
Process.put(:__bond_evaluating_assertions__, bool)
end
end