Current section

Files

Jump to
bond lib bond runtime quantifier.ex
Raw

lib/bond/runtime/quantifier.ex

defmodule Bond.Runtime.Quantifier do
@moduledoc internal: true
@moduledoc """
Runtime support for the `forall`/`exists` quantified-assertion macros defined in
`Bond.Predicates`.
Each quantifier returns a plain `boolean()` so it composes with the ordinary boolean
assertion operators (`and`, `or`, `not`, `~>`, `|||`) and keeps Bond's Dialyzer-laundering
path intact — a spec'd `boolean()` cannot be narrowed to `true`, so the falsy branch of the
surrounding `Bond.Runtime.Eval.check_assertion/3` stays reachable (see
`Bond.Predicates.__truthy__/1` for the same concern on `~>`).
The *element-level* failure detail — which element/index violated `forall`, or that no
element satisfied `exists` — cannot ride back through that boolean, so it travels through a
single per-process side channel that `Bond.Runtime.Eval` reads on the failure path.
## Side-channel lifecycle
The slot (`@failure_key`) holds at most one failure-detail map, scoped to a single assertion
expression:
* every quantifier call clears the slot on entry and writes detail only when it fails;
* `Bond.Runtime.Eval.check_assertion/3` and `check_value/3` clear the slot whenever an
assertion *passes*, so a quantifier whose `false` was absorbed into a truthy result
(`not forall(...)`, `forall(...) or other`) cannot leak stale detail into a later
assertion's failure;
* `Bond.Runtime.Eval` pops the slot when an assertion fails, attaching the detail under the
`:quantifier` key of the failure info.
When several quantifiers appear in one expression — including nested ones — the slot holds
the *last* one to fail. This is a documented best-effort limitation; for the motivating
bare-quantifier case (`@pre forall(x <- items, x > 0)`) the reported element is exact.
"""
# Single per-process slot carrying the most recent quantifier failure detail. Read and
# cleared by `Bond.Runtime.Eval` on the assertion failure/success paths via `pop/0`/`clear/0`.
@failure_key :__bond_quantifier_failure__
@typedoc """
Failure detail recorded in the side channel and surfaced on the `:quantifier` key of an
assertion failure. `:forall` carries the offending `:element` and zero-based `:index`;
`:exists` carries the element `:count` and the source text of the enumerable.
"""
@type failure ::
%{quantifier: :forall, element: term(), index: non_neg_integer(), predicate: String.t()}
| %{
quantifier: :exists,
predicate: String.t(),
count: non_neg_integer(),
enum_code: String.t()
}
@doc """
Universal quantifier: returns `true` when `fun` holds for *every* element of `enum`.
Short-circuits at the first element for which `fun` returns falsy, recording that element and
its zero-based index in the side channel before returning `false`. An empty `enum` is
vacuously `true`. `predicate_code` is the source text of the predicate (captured at
compile-time by the `Bond.Predicates.forall/2` macro) for the failure message.
A predicate that *raises* (rather than returning falsy) propagates unchanged — the quantifier
reports an unsatisfied element, not a crashing one. Guard shape-dependent predicates with the
`~>` implication operator, exactly as in multi-clause contracts.
"""
@spec forall(Enumerable.t(), (term() -> as_boolean(term())), String.t()) :: boolean()
def forall(enum, fun, predicate_code) when is_function(fun, 1) do
clear()
result =
enum
|> Stream.with_index()
|> Enum.reduce_while(true, fn {element, index}, _acc ->
if fun.(element) do
{:cont, true}
else
{:halt, {element, index}}
end
end)
case result do
true ->
true
{element, index} ->
put(%{quantifier: :forall, element: element, index: index, predicate: predicate_code})
false
end
end
@doc """
Existential quantifier: returns `true` when `fun` holds for *at least one* element of `enum`.
Short-circuits at the first element for which `fun` returns truthy. If no element satisfies
`fun` (including an empty `enum`), records the element count and returns `false`. There is no
single offending element, so the failure detail carries `:count` and `enum_code` (the source
text of the enumerable) for a "no element of … satisfies …" message.
"""
@spec exists(Enumerable.t(), (term() -> as_boolean(term())), String.t(), String.t()) ::
boolean()
def exists(enum, fun, predicate_code, enum_code) when is_function(fun, 1) do
clear()
result =
Enum.reduce_while(enum, 0, fn element, count ->
if fun.(element), do: {:halt, :found}, else: {:cont, count + 1}
end)
case result do
:found ->
true
count when is_integer(count) ->
put(%{quantifier: :exists, predicate: predicate_code, count: count, enum_code: enum_code})
false
end
end
@doc """
Reads and clears the current quantifier failure detail, returning it or `nil`.
Called by `Bond.Runtime.Eval` on the assertion-failure path to attach element-level detail to
the failure info.
"""
@spec pop() :: failure() | nil
def pop, do: Process.delete(@failure_key)
@doc """
Clears the side channel.
Called by `Bond.Runtime.Eval` whenever an assertion passes, so an absorbed quantifier failure
cannot leak into a later assertion's failure, and by each quantifier on entry.
"""
@spec clear() :: :ok
def clear do
Process.delete(@failure_key)
:ok
end
@spec put(failure()) :: :ok
defp put(detail) when is_map(detail) do
Process.put(@failure_key, detail)
:ok
end
end