Packages
bond
1.12.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 the runtime modes (see below) and return `false` when the
resolved mode for `kind` 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`.
## Runtime modes (the gate's fast path)
The per-kind runtime on/off state lives in a single `:persistent_term` entry — a map
`%{kind => true | false | :unset}` under the `:bond_runtime_modes` key — read once per
`should_evaluate?/3`. `:persistent_term` reads are lock-free and ~5x cheaper than the
`Application.get_env/3` this replaced, which mattered because the gate runs on every
contracted call (even when contracts are disabled) and the chain check multiplies the
lookups.
The term is **lazily seeded** from application env on first read: for each kind,
`Application.get_env(:bond, kind, :unset)`. So `config :bond, …` (config.exs) and
`config/runtime.exs` are both honoured — they're in app env by the time the first
contracted call runs. `:unset` (no global override) resolves to the call site's baked
`compile_default`, which carries per-module `:overrides`. The result is identical to the
old `Application.get_env(:bond, kind, compile_default)` semantics, but the app-env read
happens once instead of per call.
Runtime changes go through `Bond.Config` (which delegates to `put_mode/2` / `reset/0`
here). `Application.put_env(:bond, …)` *after* the first contracted call is no longer
picked up automatically — call `Bond.Config.reset/0` to re-seed from app env, or use the
`Bond.Config` setters. Writes go through `:persistent_term.put/erase`, which trigger a
global GC scan; this is fine for setup-time and occasional runtime toggles, not per-call.
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,
# State and transition invariants share Bond.InvariantError; the granular `kind` (kept for
# telemetry) selects the message headline. See Bond.InvariantError.
state_invariant: Bond.InvariantError,
transition_invariant: Bond.InvariantError
}
# Single persistent_term key holding the runtime modes map. Atom key (not a tuple) so the
# lookup hashes cheaply (~6 ns vs ~9 ns for a tuple key, ~30 ns for Application.get_env).
@modes_key :bond_runtime_modes
# Sentinel distinguishing "term never seeded" (trigger the lazy app-env read) from a
# seeded term whose entries may themselves be `:unset`.
@unseeded :__bond_modes_unseeded__
@kinds [:preconditions, :postconditions, :invariants, :checks]
@type assertion_fun :: (-> term())
@type compile_default :: boolean()
@type kind :: :preconditions | :postconditions | :checks | :invariants
# A kind's resolved mode in the runtime modes map: `true`/`false` for an explicit global
# setting (from app env at seed time, or from `Bond.Config`), or `:unset` meaning "no
# global override — fall back to the call site's baked `compile_default`".
@type runtime_mode :: boolean() | :unset
# A lower kind's compile-time default as it appears in the chain map. It can be `:purge` —
# not just a boolean — because a function with no `@pre`/`@post` of its own contributes
# `:purge` for that kind (e.g. an invariant-only `size/1` passes
# `%{preconditions: :purge, postconditions: :purge}`). `:purge` is treated as non-disabling
# (`:purge != false`).
@type chain_default :: boolean() | :purge
@doc """
Returns `true` when assertions of `kind` should be evaluated at this call site.
Reads the runtime modes term once (lazily seeding it from app env on first call) and
returns `false` only when the resolved mode for `kind` is exactly `false`. `compile_default`
is the per-module compile-time-resolved mode for the kind, baked into the generated call
site; it is used when the kind's runtime mode is `:unset` (no global override).
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 resolves to `false`, 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()) => chain_default()}) ::
boolean()
def should_evaluate?(kind, compile_default, chain_defaults \\ %{}) do
modes = modes()
if resolved_enabled?(modes, kind, compile_default) do
chain_clear?(modes, kind, chain_defaults)
else
false
end
end
@doc """
Returns the runtime modes map, lazily seeding it from application env on first read.
Public so `Bond.Config` can present the global runtime state; the generated call sites use
it via `should_evaluate?/3`.
"""
@spec modes() :: %{kind() => runtime_mode()}
def modes do
case :persistent_term.get(@modes_key, @unseeded) do
@unseeded -> seed_from_app_env()
modes -> modes
end
end
@doc """
Sets the runtime mode for `kind`, overriding both the app-env seed and the call site's
compile default until `reset/0`. Used by `Bond.Config`.
"""
@spec put_mode(kind(), boolean()) :: :ok
def put_mode(kind, value) when kind in @kinds and is_boolean(value) do
:persistent_term.put(@modes_key, Map.put(modes(), kind, value))
:ok
end
@doc """
Drops the runtime modes term so the next read re-seeds from current application env. Used
by `Bond.Config.reset/0` and test setup. Discards any `Bond.Config` overrides.
"""
@spec reset() :: :ok
def reset do
:persistent_term.erase(@modes_key)
:ok
end
# Builds the modes term from app env. `:unset` for kinds with no app-env entry, so they
# fall back to the call site's compile default. Idempotent under concurrent first reads:
# every caller computes the same term from the same app env.
defp seed_from_app_env do
modes = Map.new(@kinds, fn kind -> {kind, Application.get_env(:bond, kind, :unset)} end)
:persistent_term.put(@modes_key, modes)
modes
end
# Resolves whether `kind` is enabled given the modes term: an explicit `true`/`false`
# global setting wins; `:unset` falls back to the call site's `compile_default`. A missing
# key (shouldn't happen — the term always carries all kinds) also falls back.
defp resolved_enabled?(modes, kind, compile_default) do
case Map.get(modes, kind, :unset) do
:unset -> compile_default
value -> value
end != false
end
# Returns `true` when every lower-chain kind is enabled. Returns `false` as soon as one
# resolves to `false` — 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. Resolution reuses the already-read `modes` term — no further lookups —
# falling back to each lower kind's compile default when its runtime mode is `:unset`.
#
# The empty-chain case (`:preconditions`, `:checks`) short-circuits, and the non-empty case
# walks the map with `:maps.iterator/1` rather than `Enum.reduce_while/3` — the Enumerable
# protocol dispatch and iterator machinery cost ~20-25 ns per call (even for an empty map),
# which dominated the gate once the per-kind `Application.get_env`s were gone.
defp chain_clear?(_modes, _higher_kind, chain_defaults) when map_size(chain_defaults) == 0,
do: true
defp chain_clear?(modes, higher_kind, chain_defaults) do
chain_clear(modes, higher_kind, :maps.next(:maps.iterator(chain_defaults)))
end
defp chain_clear(_modes, _higher_kind, :none), do: true
defp chain_clear(modes, higher_kind, {lower_kind, lower_default, iter}) do
if resolved_enabled?(modes, lower_kind, lower_default) do
chain_clear(modes, higher_kind, :maps.next(iter))
else
maybe_log_propagation(higher_kind, lower_kind)
false
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}:
Bond.Config.enable(:#{lower_kind})
This warning is logged once per process per (higher, lower) pair.
""")
end
end
@doc """
Evaluates a single assertion result and throws an `:assertion_failure` if the result is
falsy. Returns `:ok` if the result is truthy.
Used as the building block for the lifted assertion defps emitted by
`Bond.Compiler.Assertion.assertions_body/2` and `invariants_body/2`. Centralising the
truthiness check here — where `result` is typed as `term()` at the function boundary —
prevents Dialyzer from proving the falsy branch unreachable when the caller's expression
is statically known to be `true` (e.g. `@pre is_binary(x)` on a function whose `@spec`
already narrows `x` to `binary()`). Inlining the same `if result do :ok else throw(...) end`
into the user's lifted defp emitted Pattern: `false`, Type: `true` warnings under
downstream `mix dialyzer`.
`assertion_info` is the map carrying the assertion's label/kind/file/line/etc. `binding_fun`
is a 0-arity thunk that returns the calling-function's `binding()` snapshot. It is a thunk
rather than an eager keyword list so the (potentially large) snapshot is built only on the
failure clauses — the success clause never forces it. The lifted defps emit
`fn -> binding() end` for it; see `Bond.Compiler.Assertion.assertions_body/2`.
"""
@spec check_assertion(term(), map(), (-> keyword())) :: :ok
def check_assertion(false, assertion_info, binding_fun),
do: assertion_failure(assertion_info, binding_fun.())
def check_assertion(nil, assertion_info, binding_fun),
do: assertion_failure(assertion_info, binding_fun.())
# Success clause also clears the quantifier side channel: a `forall`/`exists` whose `false`
# was absorbed into a truthy result (`not forall(...)`, `forall(...) or other`) would
# otherwise leave stale element detail that a *later* assertion's failure would mis-report.
# See `Bond.Runtime.Quantifier`.
def check_assertion(_result, _assertion_info, _binding_fun) do
Bond.Runtime.Quantifier.clear()
:ok
end
@doc """
Variant of `check_assertion/3` for `Bond.check/1` calls: returns the assertion's value on
success (so the `check expr` expression evaluates to `expr`'s value) rather than `:ok`.
Same Dialyzer-laundering motivation as `check_assertion/3`: clause-matching on `false`/`nil`
(rather than an `if` body) is what defeats Dialyzer's narrowing — a single-clause function
with `if result do ... else ... end` lets caller-inferred narrowing prove the falsy branch
dead even when the spec widens `result` to `term()`.
"""
@spec check_value(term(), map(), (-> keyword())) :: term()
def check_value(false, assertion_info, binding_fun),
do: assertion_failure(assertion_info, binding_fun.())
def check_value(nil, assertion_info, binding_fun),
do: assertion_failure(assertion_info, binding_fun.())
# See `check_assertion/3`'s success clause — clear the quantifier side channel on the
# passing path so absorbed quantifier failures can't leak into a later assertion.
def check_value(value, _assertion_info, _binding_fun) do
Bond.Runtime.Quantifier.clear()
value
end
@doc """
Coverage-recording variant of `check_assertion/3` (issue #56). Records the evaluation via
`Bond.Coverage.record/2` and then delegates unchanged. The compiler emits this in place of
`check_assertion/3` only when contract coverage is enabled at compile time
(`config :bond, coverage: true`), so a normal build never pays for it.
`result` is typed `term()` for the same Dialyzer-laundering reason as `check_assertion/3`: the
caller passes the user's (possibly statically-`true`) expression into a `term()` parameter, so
Dialyzer cannot narrow it and prove the failure path dead.
"""
@spec check_assertion_covered(term(), map(), (-> keyword())) :: :ok
def check_assertion_covered(result, assertion_info, binding_fun) do
Bond.Coverage.record(assertion_info, result)
check_assertion(result, assertion_info, binding_fun)
end
@doc """
Coverage-recording variant of `check_value/3` (issue #56), for `Bond.check/1`. Records the
evaluation, then delegates unchanged (returning the checked value on success). Emitted in place
of `check_value/3` only when coverage is enabled at compile time.
"""
@spec check_value_covered(term(), map(), (-> keyword())) :: term()
def check_value_covered(result, assertion_info, binding_fun) do
Bond.Coverage.record(assertion_info, result)
check_value(result, assertion_info, binding_fun)
end
@spec assertion_failure(map(), keyword()) :: no_return()
defp assertion_failure(assertion_info, binding) do
assertion_info =
case Bond.Runtime.Quantifier.pop() do
nil -> assertion_info
detail -> Map.put(assertion_info, :quantifier, detail)
end
throw({:assertion_failure, Map.put(assertion_info, :binding, Enum.sort(binding))})
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
@doc """
Evaluates a precondition group purely as a predicate: returns `true` if every assertion holds,
`false` on the first violation — *without* raising, firing a `[:bond, :assertion, :failure]`
telemetry event, or running the function body.
`preconditions_fun` is the same 0-arity thunk `evaluate_preconditions/1` receives (it invokes
the lifted precondition defp, which throws `{:assertion_failure, info}` on the first failing
assertion). This is the non-raising, side-effect-free evaluator behind the public
`__bond_precondition__/3` shim, used by `Bond.PropertyTest` to *filter* generated inputs down to
those that satisfy `@pre` (#36): a discarded out-of-precondition input is a generation miss, not
a contract violation, so it must not raise or emit failure telemetry.
Mirrors the private `run_group/1` used by `evaluate_pre_weaken/3`: only the
`{:assertion_failure, _}` throw counts as non-satisfaction. A genuine exception from a malformed
assertion — or a parameter-pattern mismatch in a single-clause function's lifted defp, whose
head reproduces the user's pattern — propagates unchanged.
"""
@spec precondition_satisfied?(assertion_fun()) :: boolean()
def precondition_satisfied?(preconditions_fun) when is_function(preconditions_fun, 0) do
preconditions_fun.()
true
catch
{:assertion_failure, _info} -> false
end
@doc """
Evaluates an Eiffel-style *weakened* precondition: the inherited precondition group OR the
implementation's `@pre_weaken` group (#16).
`inherited_fun` and `weaken_fun` are 0-arity thunks, each wrapping a raw
`Bond.Compiler.Assertion.assertions_body`-style sequence (a conjunction that throws
`{:assertion_failure, info}` on its first failing assertion). The inherited group is tried
first; if it passes, the precondition holds. Only if it *fails* (returns falsy) is the
weakening group tried — disjunction can only weaken, never strengthen, which is precisely the
Liskov-safe direction for preconditions. If neither group holds, a single combined
`{:assertion_failure, combined_info}` is thrown for the surrounding `evaluate_preconditions/1`
to turn into a `Bond.PreconditionError` (telemetry, stacktrace pruning, and recursion guard are
all shared with the ordinary path, since this runs inside the lifted precondition defp).
A group that *raises* (rather than returning falsy) propagates — the weakening alternative is
for when the inherited precondition is not *satisfied*, not for when a malformed assertion
crashes. Authors guard shape-dependent halves with the `~>` implication operator, exactly as
in multi-clause contracts.
The combined failure reuses the binding captured by the weakening group (impl-scoped, so the
reported variables are the ones the author named in `@pre_weaken`).
"""
@spec evaluate_pre_weaken((-> term()), (-> term()), map()) :: term()
def evaluate_pre_weaken(inherited_fun, weaken_fun, combined_info)
when is_function(inherited_fun, 0) and is_function(weaken_fun, 0) and is_map(combined_info) do
case run_group(inherited_fun) do
:ok ->
:ok
{:failed, _inherited_info} ->
case run_group(weaken_fun) do
:ok ->
:ok
{:failed, weaken_info} ->
throw({:assertion_failure, Map.put(combined_info, :binding, weaken_info.binding)})
end
end
end
# Runs one assertion group's raw check sequence, converting its first-failure throw into a
# `{:failed, info}` tag rather than letting it propagate — so `evaluate_pre_weaken/3` can fall
# through to the other group. A group whose every assertion passes returns `:ok`. Only the
# `{:assertion_failure, _}` throw is caught; a genuine exception from a malformed assertion
# propagates unchanged.
defp run_group(group_fun) do
group_fun.()
:ok
catch
{:assertion_failure, info} -> {:failed, info}
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
@doc """
Evaluates a `Bond.Server` module's `@state_invariant` / `@transition_invariant` check around a
callback (#34), attributing any failure to that callback.
Mirrors `evaluate_invariants/1` (the kind — `:state_invariant` or `:transition_invariant` —
comes from the assertion and selects the error type), except the thrown `assertion_info` is
enriched with `:function => function_info` on the failure path. These invariants are
module-level — shared across every callback — so the callback they were checked around is
supplied here rather than baked into the assertion, and the enrichment runs only when an
invariant actually fails, so the passing path stays allocation-free.
"""
@spec evaluate_server_invariants(assertion_fun(), {atom(), non_neg_integer()}) :: term()
def evaluate_server_invariants(check_fun, function_info)
when is_function(check_fun, 0) and is_tuple(function_info) do
evaluate_assertions(check_fun, &Map.put(&1, :function, function_info))
end
@doc """
Evaluates a protocol contract's assertions, attributing any failure to the protocol and the
implementation the call resolved to.
Used by the dispatch wrapper `Bond.Protocol` emits in a protocol module. Identical to
`evaluate_preconditions/1` / `evaluate_postconditions/1` (the kind comes from the assertion
itself), except that on failure the thrown `assertion_info` is enriched with
`:source_protocol` (the protocol module) and `:impl` (the resolved implementation module, via
`protocol.impl_for(subject)`, or `nil`). The impl is resolved only on the failure path, so an
enabled-but-passing contract pays nothing for it. `subject` is the value dispatch keyed on —
the protocol function's first argument.
"""
@spec evaluate_protocol_assertions(module(), term(), assertion_fun()) :: term()
def evaluate_protocol_assertions(protocol, subject, assertions_fun)
when is_atom(protocol) and is_function(assertions_fun, 0) do
evaluate_assertions(assertions_fun, fn assertion_info ->
assertion_info
|> Map.put(:source_protocol, protocol)
|> Map.put(:impl, resolve_impl(protocol, subject))
end)
end
# `impl_for/1` is generated on every protocol module (and survives consolidation); guard
# against the unexpected so diagnostics can never mask the original contract failure.
defp resolve_impl(protocol, subject) do
protocol.impl_for(subject)
rescue
_ -> nil
end
@doc """
Runs the lifted invariant check against a function's return value when that value carries
the struct the invariants are declared on.
Detects the `%module{}` and `{:ok, %module{}}` return shapes *here*, with `result` typed
as `term()`, rather than via a `case` spliced into the using module. A struct `case`
emitted into the user's module would let Elixir's type checker (1.18+) prove the struct
clauses unreachable for functions that return other shapes (e.g. a `size/1` returning an
integer), raising "the following clause will never match" — a warning that becomes a hard
error under `--warnings-as-errors`. Keeping the match in this separately-compiled,
`term()`-typed module avoids that while preserving identical runtime behaviour.
`check_fun` bridges back to the using module's private lifted-invariant defp. Any other
return shape falls through with no check.
"""
@spec check_struct_invariant(term(), module(), (struct() -> term())) :: :ok
def check_struct_invariant(result, module, check_fun)
when is_atom(module) and is_function(check_fun, 1) do
case extract_subject(result, module) do
{:ok, subject} -> evaluate_invariants(fn -> check_fun.(subject) end)
:error -> :ok
end
:ok
end
defp extract_subject(result, module) do
cond do
is_struct(result, module) -> {:ok, result}
match?({:ok, inner} when is_struct(inner, module), result) -> {:ok, elem(result, 1)}
true -> :error
end
end
# `enrich` is applied to the thrown `assertion_info` before the failure event and exception
# are built — the seam `evaluate_protocol_assertions/3` uses to attach protocol/impl info on
# the failure path only. Defaults to the identity for ordinary `@pre`/`@post`/`@invariant`.
defp evaluate_assertions(assertions_fun, enrich \\ &Function.identity/1) do
try do
with_recursion_check(assertions_fun)
catch
{:assertion_failure, %{kind: kind} = assertion_info} ->
assertion_info = enrich.(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