# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [0.17.0] - 2026-05-30
0.17.0 closes the longest-standing bug surfaced by real-world dogfooding:
multi-clause functions whose first body clause had shape-specific
patterns silently broke callers using a different shape. The fix is a
per-clause wrapper redesign that preserves Elixir's natural multi-clause
dispatch, plus a new rule that all clauses must agree on the top-level
parameter name at each position when Bond is attaching contracts.
0.17.0 also fixes a latent semantic bug in `Bond.Predicates.~>/2`: the
implication operator was a `def`, so both sides were eagerly evaluated.
The natural "shape-dependent assertion" pattern
(`is_struct(x, Mod) ~> (x.id > 0)`) crashed on non-struct inputs to the
antecedent. `~>` is now a `defmacro` that short-circuits.
### Breaking changes (minor)
- **`Bond.Predicates.~>/2` is now a macro, not a function.** The
expansion is `if antecedent, do: !!consequent, else: true`, so the
consequent is only evaluated when the antecedent is truthy. This
matches the logical reading of implication and makes
shape-dependent assertions safe to write.
# Was: both sides evaluated eagerly — `String.length(x)` raised
# FunctionClauseError when `x` wasn't a binary.
@pre is_binary(x) ~> (String.length(x) > 0)
# Now: `String.length(x)` is only evaluated when `is_binary(x)`
# is truthy. Same source, correct semantics.
The migration impact is limited: code that passed `~>` as a function
capture (`&Bond.Predicates.~>/2`) no longer compiles. Use a function
wrapper or the underlying `implies?/2` (still a function and still
eagerly evaluated) instead. The infix usage that's common in
contracts is unchanged.
- **Multi-clause functions with contracts must agree on top-level
parameter names across all clauses.** Heterogeneous naming raises
`CompileError` at the function's compile site. Pre-0.17.0 Bond used
the first clause's params for the wrapper head verbatim, silently
breaking callers whose shape matched a non-first clause; the new
rule makes the constraint explicit and pushes naming consistency
(often a readability win regardless of Bond).
# Was (silently broken): callers passing strings hit
# FunctionClauseError inside Bond's generated code.
def lookup(conn, %Game{} = g, %GameFilm{} = f), do: ...
def lookup(conn, league, conference) when is_binary(league), do: ...
# Now: rename for consistent positional meaning across clauses.
def lookup(conn, %Game{} = resource, %GameFilm{} = scope), do: ...
def lookup(conn, resource, scope) when is_binary(resource), do: ...
Wildcard clauses (`def f(_)`) and literal-pattern clauses
(`def f(0)`) don't bind a top-level name at that position — they
adopt whatever name a sibling clause provides. So the common
`def try_init(_)`-paired-with-`def try_init(capacity)` pattern
works unchanged.
For shape-dependent assertions across clauses, use the `~>`
implication operator (which now short-circuits, per above).
Per-clause contracts may be added in a future release if the
consistent-naming restriction turns out to bite real code.
### Added
- **`Bond.Compiler.Clauses`** — new internal module owning clause-
shape utilities: `top_level_names/1`, `canonical_names/1`,
`assert_clauses_agree!/3` (the validator), `rewrite_clause_params/3`
(canonical-name binding + underscore-prefix of unused names), and
`underscore_prefix_unused/2`.
- **`Bond.Compiler.ClauseWrapper`** — new internal module owning
per-clause wrapper emission. Extracted from `AnnotatedFunction`
(which is on the FSM's hot path) to keep that file small and avoid
the parallel-compile race the project first encountered in 0.13.0.
### Changed
- **Wrapper emission switches from single-wrapper to per-clause.**
For an N-clause user function, Bond now emits N wrapper clauses,
each preserving the user's pattern (with destructured names
underscore-prefixed where the wrapper body doesn't reference them).
Elixir's natural multi-clause dispatch routes each call to the
appropriate user clause via `super/N`. Wrong-shape inputs raise
`FunctionClauseError` at the wrapper layer, matching the pre-Bond
behaviour.
- **Lifted assertion defps' parameter heads diverge by clause count.**
- Single-clause functions keep the user's pattern in the lifted
defp head, so contracts can still reference destructured names
from the head (e.g. `current_count` from
`%__MODULE__{count: current_count} = state` — the
contracts-and-concurrency guide example works unchanged).
- Multi-clause functions use the canonical top-level names as bare
vars. Contracts can only reference those names; shape-dependent
assertions use `~>`.
- **Destructure-in-head wrapper warnings (the original #3 from the
Photon dogfood) are silenced.** Bond's per-clause wrapper now
underscore-prefixes any destructured name the wrapper body doesn't
reference. The lifted defp's pattern (for single-clause functions)
still binds those names, so contract-side access is unaffected.
### Internal
- **`Bond.Compiler.Invariants` simplifications.** The destructure-only
invariant handling that ran in 0.16.x is subsumed by the canonical-
name rewrite — `Invariants.rewrite_call_params/2` and
`Invariants.params_split/3` are no longer called from emission.
They remain in the module for now; cleanup deferred to a later
release.
- **`Bond.Compiler.AnnotatedFunction` shrunk from 448 → 430 lines**
via the ClauseWrapper and Clauses extractions, well below the
historical baseline where the parallel-compile race surfaced.
- **Test fixture migration.** The existing `BondTest.InvariantSmoke.
try_new/1` (wildcard adopts canonical) and `BondTest.Stack.new/N`
(all clauses agree on `capacity`) work unchanged under the new
rule. The unit-test fixture in `Bond.Compiler.AnnotatedFunctionTest`
(previously `list`/`map`) migrated to consistent `input`.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.16.2] - 2026-05-28
A patch release covering eight issues surfaced by dogfooding Bond 0.16.1
on a real-world Elixir umbrella application (Photon, ~200+ modules). Six
fixes bundle here; two (#2 wrapper-head shape leak, #3 destructure-in-
head unused warnings) are deferred to 0.17.0 pending a design
conversation.
### Changed
- **Remote function calls are now valid as the outermost expression of
an assertion.** Pre-0.16.2, `@pre String.starts_with?(x, "foo")` was
rejected by `Bond.Compiler.Assertion.is_assertion_expression/1` —
the AST's head is a `{:., _, _}` 3-tuple, not an atom, so the guard
failed. Workaround was an `== true` suffix on every such assertion.
The relaxed guard accepts the remote-call shape, including
`Map.has_key?(m, :k)`, `Enum.all?(xs, &f/1)`,
`String.starts_with?(s, "prefix")`, and Erlang calls
(`:erlang.is_atom`).
- **No `@doc` emission on `defp`.** Contracts on private helpers
previously triggered Elixir's "@doc is always discarded for private
functions" warning on every contracted defp, making the combination
unusable without compile-time noise. `Bond.Compiler.ContractDocs.
doc_clauses/4` now short-circuits for `:defp` kind. The contracts
themselves continue to fire — the warning was the only blocker.
- **`Bond.Predicates` moduledoc gains an "Operator precedence"
section** documenting the `~>` / `<~` left-associativity trap.
`A ~> pattern <~ B` parses as `(A ~> pattern) <~ B`, where the LHS
of `<~` becomes an arbitrary expression containing `_` and fails to
compile. The fix is parens around the inner operator. Same trap
surfaced as a boxed callout in the main moduledoc's Assertion Syntax
section so readers see it before they fall into it.
- **Telemetry section** gains a concrete metadata-map example showing
the `{name, arity}` shape of `:function`, the sorted-binding-list
shape of `:binding`, and a note on `:assertion_id` stability for
aggregation pipelines.
- **Assertion Syntax section** in the moduledoc now shows remote-call
examples and explicitly notes which forms aren't valid (bare
literals, bare variables, non-call expressions).
### Fixed
- **`@pre is_binary(x), positive: x > 0`** (bare assertion mixed with
a labelled one) and `@pre is_integer(x), x > 0` (two bare assertions
in a single call) previously fell through to Kernel's `@/1` and died
with "expected 0 or 1 argument for @pre, got: 2" — a confusing error
that didn't point at the parse issue. Bond now matches these shapes
at the macro layer and raises a clear `CompileError` suggesting
either label-every-assertion (keyword-list form) or separate
`@pre`/`@post` lines. Same catch-all added for `@invariant`.
- **Bond-shaped diagnostics on malformed assertions.** When the user
wrote an assertion that didn't satisfy `is_assertion_expression/1`
(`@pre 42`, `@pre :foo`, `@pre "hello"`), Bond previously surfaced
a bare `FunctionClauseError` from `Assertion.new/5` with a
stacktrace that dumped the full `Macro.Env`. New
`Bond.Compiler.Assertion.validate_expression!/2` is called from both
`register_assertion/5` and `register_invariant/4`, and raises
`CompileError` with the env's file/line, the expression's source
(via `Macro.to_string/1`), and a one-sentence hint at valid forms.
### Internal
- **Test coverage filled across each fix** (+36 tests, 256 total):
- 6 unit tests in `assertion_test.exs` for the relaxed AST guard
plus the new `validate_expression!/2` validator.
- 8 behavioural tests in a new `BondTest.RemoteCallAssertions`
fixture proving remote-call assertions work end-to-end in `@pre`,
`@post`, `@invariant`, and `check/1` — both success and violation
paths.
- 4 behavioural + diagnostic tests for `defp` contracts, including
a `capture_io(:stderr, ...)` assertion that no `@doc`-discarded
warning fires during compilation.
- 9 behavioural tests covering the new bare-vs-labelled
`CompileError` catch-alls and verifying all five existing valid
forms still compile cleanly.
### Deferred (to 0.17.0)
- **#2 wrapper-head shape leak.** Bond's override head uses the first
body clause's params verbatim, so multi-clause functions with a
shape-specific first clause silently break callers using a different
shape (a `def fn(conn, %Game{}, %GameFilm{})` clause alongside a
sibling `def fn(conn, league, conference) when is_binary(league)`
clause will misroute string callers). The right fix needs a design
conversation on whether to use a shape-neutral wrapper with
restricted contract refs vs a per-clause wrapper that preserves
dispatch faithfully.
- **#3 destructure-in-head wrapper warnings.** When the first body
clause has destructure like `def f(%Mod{a: x, b: y} = z)` and the
wrapper body uses only `z`, Elixir warns about unused `x`/`y`.
Partially subsumed by #2's resolution.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.16.1] - 2026-05-27
A patch release covering a 1.0-prep test-coverage audit (no behavioural
change, just locking down behaviour that wasn't directly tested) plus
a refresh of the supporting guides for content that had drifted out of
date with 0.16.0.
### Changed
- **`guides/about.md` — full rewrite.** The previous version's feature
TODO list still showed conditional compilation (shipped in 0.10/0.11)
and invariants (shipped in 0.13) as unchecked items, and the framing
read like marketing copy. New version is structured as "what Bond is,
when to reach for it, background" — same length, every paragraph
carrying information.
- **`guides/getting-started.md` — `@invariant` section added.** The
tutorial previously mentioned `@invariant` only in "Next steps." A
reader following it linearly would never learn there was a third
contract kind. New section between "Inline checks" and "Disabling
contracts in production" introduces it with a `BoundedStack` example
and the `subject` binding, then points at the moduledoc for the full
reference. The intro line at the top mentions `@invariant` alongside
`@pre`/`@post`/`check/1`. The disabling-in-production config snippet
now lists `:invariants` (was listing three of four keys).
### Fixed
- **`guides/faq.md` — "When does Bond check invariants?"** description
brought current. The previous text said destructure-only function
heads (`def foo(%__MODULE__{f: v}, ...)`, no `= name`) emit a
compile-time warning and skip the pre-check. The 0.16.0 release
lifted that restriction — Bond now rewrites the override clause to
capture the struct under a generated name and the pre-check fires.
Multi-struct heads are also noted (weren't previously).
- **`guides/getting-started.md` — dead anchor.** The "Next steps" link
to the Invariants section used the pre-0.16.0 anchor
`#module-invariants`; updated to `#module-invariant-for-struct-modules`
matching the renamed section.
### Internal
- **Test coverage filled across seven gaps from a 1.0-prep audit**
(+16 tests, 220 total; all green). No behavioural change — each
fill verifies behaviour that was already in place but lacked a
direct test:
* **Invariant telemetry.** `[:bond, :assertion, :failure]` fires
with `:kind => :invariant` on invariant violations. Documented
since 0.13.0; previously only the other three kinds had
assertions on the event.
* **`@invariant` runtime modes.** Two tests cover (a) `put_env
:bond, :invariants, false` skips evaluation, and (b) flipping
back to `true` re-engages it. The runtime-toggle path was
tested for `@pre`/`@post` but not `@invariant`.
* **Compound `and` guards.** Behavioural confirmation that
`is_struct(x, __MODULE__)` nested inside an `and` guard
triggers the pre-invariant check. (Compiler-level detection
was already covered.) The `or` case is deliberately not
covered — it's a latent unsafe pattern worth a separate
design discussion.
* **No-struct heads.** Behavioural confirmation that a function
whose head doesn't expose the struct silently skips pre-
invariant evaluation — passing non-struct arguments returns
cleanly rather than crashing on a `subject.<field>` access.
* **Migration `CompileError`s.** The legacy `@invariant <name>,
<expr>` and the two arity-2 `check` shapes (removed in 0.16.0)
now have direct assertions that they raise `CompileError` with
the migration message at the call site.
* **`Bond.Test.assert_check_violation/2`.** The helper existed
alongside its `precondition`/`postcondition`/`invariant`
siblings but had no test.
* **`old(...)` runtime integration.** Compiler-level extraction
and precompilation were covered; the runtime path (does the
snapshotted value end up correctly bound when the postcondition
evaluates?) had no direct test. New `Bond.OldRuntimeTest`
covers success and failure paths plus the captured `binding()`
at failure.
- **Coverage audit findings worth keeping in mind for future
releases** (not addressed in 0.16.1):
* Compound `or` guards containing `is_struct(_, __MODULE__)` are
a latent unsafe pattern. Bond's detection recognises `x` as the
struct parameter, but the pre-invariant fires unconditionally —
so a runtime input matching a non-struct alternative crashes in
the invariant body rather than raising a clean
`FunctionClauseError`.
* Relatedly, the override clause doesn't reproduce the user's
function-head guard. Calling `Smoke.reverse(5)` (where
`reverse` has `when is_struct(stack, __MODULE__)`) hits Bond's
pattern-less override, fires the pre-invariant against the
integer, and crashes inside the invariant body before super
dispatches to the user's def for the proper
`FunctionClauseError`.
Neither issue surfaces in normal use (callers pass arguments of
the right shape) — they're worth a fix pass before 1.0 but not
shippable as a patch.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.16.0] - 2026-05-26
0.16.0 is the first 1.0-prep release. It tightens the public API in two
places where the surface had accumulated friction: `@invariant` drops its
required binding-name argument in favour of an implicit `subject` binding,
and `check/2` drops its two string-label forms in favour of `check expr`
and `check label: expr`. Both legacy shapes now raise `CompileError` at the
call site with a migration message.
### Breaking changes (minor)
- **`@invariant <name>, <expr>` was removed.** The new form is `@invariant
<expr_or_kw>` — no binding-name argument. Invariant expressions reference
the implicit `subject` binding, which Bond rebinds at every check site to
whichever struct parameter the function head exposes (detected
automatically across `%__MODULE__{} = name` patterns, `is_struct(name,
__MODULE__)` guards, and `%__MODULE__{...}` destructures).
# Was:
@invariant stack,
non_negative_capacity: stack.capacity >= 0,
size_within_capacity: length(stack.items) <= stack.capacity
# Now:
@invariant non_negative_capacity: subject.capacity >= 0,
size_within_capacity: length(subject.items) <= subject.capacity
Function bodies don't change — `def push(%__MODULE__{} = stack, item)`
keeps its parameter named `stack`; Bond detects and rebinds `subject` to
it automatically. The legacy 2-arg shape raises a `CompileError` with the
migration message.
- **`check/2` was removed.** The two string-label forms (`check "label",
expr` and `check expr, "label"`) are gone — they were redundant with the
keyword-list form, which already carries a label:
# Was:
check "x is a number", is_number(x)
check is_number(x), "x is a number"
# Now:
check x_is_number: is_number(x)
`check expr` (bare) and `check label: expr` (keyword) are the two
remaining forms. The legacy 2-arg shape raises a `CompileError` with the
migration message.
### Added
- **Multi-struct heads in `@invariant`.** `def merge(%__MODULE__{} = a,
%__MODULE__{} = b)` now triggers invariant checks on *both* struct
parameters in left-to-right order, with `subject` rebinding to each in
turn. Previously only the first detected struct param was checked.
- **Destructure-only heads in `@invariant`.** `def head(%__MODULE__{items:
[first | _]})` (no `= name`) now participates in pre-invariant checks.
Bond rewrites the override clause head to add a capturing binding
(`%__MODULE__{items: [first | _]} = __bond_subject_0__`) so the struct
passes cleanly to the lifted invariants defp. Previously this shape was
skipped silently with a documented (but unimplemented) warning.
This also closes a latent bug in the override emission: `super(...)`
previously spliced raw destructure patterns as expressions, which would
fail at compile time on patterns like `[h | _]` if a user had ever tried
it with `@pre`/`@post`. The capture rewrite passes the original input
through cleanly.
- **`Bond.Compiler.Invariants.detect_struct_params/2`** — internal helper
that finds every struct-bearing parameter in a function head, returning a
list of `{:bound, var, idx}` or `{:destructure, idx}` descriptors.
Replaces the single-struct `find_struct_arg/2` removed below.
### Changed
- **Doc-generation logic extracted into `Bond.Compiler.ContractDocs`.**
Pure refactor — no user-visible change. Shaves ~80 lines off
`Bond.Compiler.AnnotatedFunction`, which is on the FSM's hot path. A
shorter `AnnotatedFunction` reduces the window for the parallel-compile
race first encountered (and partially mitigated) in 0.13.0.
- **`Bond.Compiler.Assertion` drops the `:binding_name` field.** The
invariant body now hardcodes the `subject = bond_invariant_value` rebind.
The struct shrinks from 8 fields to 7.
- **`Bond.Compiler.Invariants` simplified.** Removed the legacy
single-struct helpers `find_struct_arg/2`, `struct_arg/2`,
`pre_invariant_stmts/5`, and the supporting AST walkers. New emission
uses `detect_struct_params/2` + `all_pre_invariant_stmts/5` +
`rewrite_call_params/2` end-to-end.
- **Moduledoc reorganised.** Sections regroup as "what you write" (Usage →
Assertion syntax → `@invariant` → `check/1` → `old`) then "how you
operate" (Documenting contracts → Conditional compilation → Telemetry →
PBT). The 0.10 → 0.11 migration table is dropped, and the long Agent
race-condition narrative in `old` moves to the
`contracts-and-concurrency` guide.
- **Telemetry `:kind` documentation** updated to include `:invariant` (the
event was already emitted since 0.13.0; the docs were stale).
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.15.0] - 2026-05-25
0.15.0 closes a correctness gap in conditional compilation: previously
`:preconditions`, `:postconditions`, and `:invariants` could be toggled
independently in any combination, including combinations that produced
diagnostically-misleading errors (e.g. postconditions on while
preconditions are off — a "postcondition failure" might really mean
the caller broke their contract, not the function).
0.15.0 enforces the natural chain `preconditions ≤ postconditions ≤
invariants` both at compile time and at runtime. `:checks` remains
independent of the chain.
### Breaking changes (minor)
- **Compile-time validation of `:purge` combinations.** `:purge` on a
lower kind now requires `:purge` on every higher kind in the chain.
`Bond.Compiler.resolve_config/3` raises `CompileError` with an
explanation otherwise.
Migration: if you used `config :bond, preconditions: :purge` without
also purging postconditions/invariants, choose one:
# Was:
config :bond, preconditions: :purge
# Option A — also purge the chain (preserves the original intent
# if you wanted zero overhead):
config :bond,
preconditions: :purge,
postconditions: :purge,
invariants: :purge
# Option B — runtime-disable instead of purge (keeps the code,
# operator can flip on at runtime):
config :bond,
preconditions: false
`false` is unaffected — runtime-disabling a single kind is
unchanged. Only `:purge` participates in the compile-time check.
### Added
- **Runtime chain propagation.** When a lower kind is `false` at runtime
(`Application.put_env(:bond, :preconditions, false)`), every higher
kind is also skipped automatically, regardless of its own setting.
Enforced in `Bond.Runtime.Eval.should_evaluate?/3` via the new
optional third argument carrying the compile-time defaults of every
lower kind.
- **One-time-per-process propagation log.** The first time a higher
kind is skipped because a lower one is runtime-off, Bond emits a
`Logger.warning` describing the chain constraint, the offending
pair, and the `Application.put_env` invocation that would bring the
higher kind back. Deduped per (higher, lower) pair via a
Process-dictionary marker — long-running OTP processes get exactly
one warning per pair.
### Changed
- `Bond.Runtime.Eval.should_evaluate?/2` is now `should_evaluate?/3`
with an optional `chain_defaults` map; the 2-arity call still works
via default and is unchanged behaviour-wise for `:preconditions` and
`:checks` (both have no lower kinds).
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.14.0] - 2026-05-24
0.14.0 adds **`Bond.PropertyTest`** — a property-based testing layer that
uses Bond's contracts as the oracle. The hard part of PBT is usually
writing the predicate that distinguishes right from wrong outputs;
contracts already supply that at every call site. PBT just feeds random
inputs through already-instrumented code.
### Added
- **`Bond.PropertyTest.contract_holds/2`** — single macro, two forms:
- **Form 1 (single function).** `contract_holds &Mod.fn/N, args: [gen0, ...]`
expands to a property block that calls the function with random
arguments and lets Bond's runtime contracts fail the property on
any violation. StreamData shrinks to the minimal counterexample.
- **Form 2 (module sequence).**
`contract_holds Module, constructors:, transformers:, observers:`
expands to a property block that generates random sequences of
operations over a struct module and runs them. State is threaded
through transformers; observers don't advance state but the
pre-invariant still fires. The module's `@invariant`s are the
oracle. Supports `%Mod{}` and `{:ok, %Mod{}}` return shapes;
`{:error, _}` terminates the sequence cleanly. Common option
`:name` overrides the auto-generated property description.
The macro dispatches by first-arg AST shape (function reference vs
module alias).
- **`use Bond.PropertyTest`** — brings in `ExUnitProperties` and imports
the `contract_holds` macro. Raises a `CompileError` at the use site
with installation instructions if `:stream_data` isn't available.
- **`Bond.PropertyTest.Sequence`** — internal helper module owning the
sequence generator and runner used by Form 2.
- New FAQ entry: "How does Bond compose with StreamData /
property-based testing?".
### Changed
- `:stream_data` moves from `only: [:dev, :test]` to a regular dep with
`optional: true`. Users who want PBT now add `{:stream_data, "~> 0.6"}`
to their own deps; users who don't pay no cost.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.13.0] - 2026-05-23
0.13.0 adds **`@invariant`** declarations for struct modules — module-scoped
properties that hold across every public function in the struct's defining
module. Where `@pre`/`@post` constrain a single function call, `@invariant`
constrains the struct itself.
### Added
- **`@invariant <name>, <kw_or_expression>`** annotation. Same shape as
`@pre`/`@post`: a labelled keyword-list of assertions, or a single
unlabelled expression. The first argument is the variable name the
expression refers to (e.g. `stack` in
`@invariant stack, length(stack.items) <= stack.capacity`).
Invariants are checked at the boundaries of every public function in the
module:
- **On entry**, when the function head pattern-matches `%__MODULE__{} = name`
or has an `is_struct(name, __MODULE__)` guard.
- **On exit**, against the return value if it's `%__MODULE__{}` or
`{:ok, %__MODULE__{}}`. Other return shapes fall through with no check.
- **Never for `defp`** — private functions are exempt by the Eiffel
convention (they often hold transiently-invalid state).
When a function destructures `%__MODULE__{...}` in its head without binding
the whole struct to a variable, Bond emits a compile-time warning suggesting
`%__MODULE__{...} = name` to enable the pre-check.
- **`Bond.InvariantError`** — new exception parallel to
`PreconditionError`/`PostconditionError`/`CheckError`. Raised on invariant
violation; carries the same metadata shape.
- **`Bond.Test.assert_invariant_violation/2`** — ExUnit helper mirroring the
existing pre/post/check helpers.
- **`:invariants` conditional-compilation key.** Joins `:preconditions`,
`:postconditions`, and `:checks`. Same `true | false | :purge` value space;
same runtime toggleability via `Application.put_env/3`; same `:overrides`
and `use Bond, invariants: …` support.
- **`Bond.Compiler.Invariants`** — new internal module owning the invariant
emission logic (struct-arg detection, pre-/post-invariant call sites, the
lifted invariants defp). Kept separate from `Bond.Compiler.AnnotatedFunction`
for separation of concerns and to avoid parallel-compile scheduling issues
with the larger combined file.
### Changed
- `[:bond, :assertion, :failure]` telemetry events now also fire for invariant
violations, with `:kind => :invariant` in the metadata. No subscriber
changes are needed — existing handlers attached to the event automatically
pick up the new kind.
- The internal `Bond.Compiler.Assertion` struct gains a `:binding_name` field,
populated only on `:invariant` assertions from the declaration's first
argument.
- `Bond.Compiler.AnnotatedFunction` gains an `:invariants` field plus
`put_invariants/2` and `has_invariants?/1` helpers. `override?/1` widens to
emit overrides for public functions in modules with `@invariant`s, even
when the function has no per-function `@pre`/`@post`.
- `Bond.Compiler.CompileStateFSM` tracks module-scoped invariants alongside
the per-function preconditions/postconditions. Invariants don't transition
the FSM into `:contracts_pending` (they don't attach to a "next function")
and aren't flushed by function definitions.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.12.0] - 2026-05-22
0.12.0 lands two internal-shape changes that compose on top of the
0.11.0 conditional-compilation work: contract closures move out of
override clauses into named private functions on the user's module
(reducing injected code per contract'd function), and `:telemetry`
events fire on assertion failures.
### Added
- **`[:bond, :assertion, :failure]` telemetry event.** Fires once per
contract violation — `@pre`, `@post`, or `check` — immediately before
the corresponding `Bond.PreconditionError` / `Bond.PostconditionError`
/ `Bond.CheckError` is raised. Single event family for all three
kinds; consumers filter on the `:kind` metadata. Measurements carry
`:system_time` and `:monotonic_time`; metadata carries `:kind`,
`:module`, `:function`, `:label`, `:expression`, `:assertion_id`,
`:file`, `:line`, and `:binding`. See the new "Telemetry" section in
the `Bond` moduledoc / README. `{:telemetry, "~> 1.0"}` is now a
regular dependency.
- **`Bond.Runtime.Eval.should_evaluate?/2`** — internal helper that
performs the `Application.get_env/3` runtime guard. Used by the
emission shape (see "Internal" below) to avoid allocating the
assertion-evaluation closure when the runtime guard says skip.
### Changed
- **Per-function assertion closures are lifted into named `defp`s** on
the using module: `__bond_preconditions__<fun>__<arity>` and
`__bond_postconditions__<fun>__<arity>`. The override clause itself
is now a small wrapper that calls these via
`Bond.Runtime.Eval.evaluate_preconditions/1` /
`evaluate_postconditions/1`. The big inline assertion-evaluation AST
that used to be re-emitted into every override is gone; the BEAM
carries one tiny override + one defp per non-purged kind, rather
than the whole eval body inlined per function.
- **Runtime guard moved into `Bond.Runtime.Eval`.** The override calls
`should_evaluate?(:preconditions, <compile_time_mode>)` and only
builds the assertion-evaluation closure when that returns `true`.
The `Application.get_env/3` lookup logic lives entirely in
`Bond.Runtime` rather than being inlined at every contract'd
function.
- **`Bond.check/1,2` routes through the same throw/catch path as
`@pre`/`@post`.** All three kinds now produce
`{:assertion_failure, info}` throws caught by `Bond.Runtime.Eval`,
which fires the telemetry event and raises. This unifies the
plumbing across the three kinds; previously `check` raised inline.
- **Stacktrace pruning** now also filters frames whose function name
starts with `__bond_` (the lifted defps), so failures continue to
point at the user's call site rather than into Bond-generated
plumbing.
- **Benchmark** on the project fixture
(`bench/runtime_check_overhead.exs`, trivial `@pre is_number(x)` in a
tight loop):
| mode | 0.11.0 | 0.12.0 |
|---------|----------|----------|
| `:purge` | ~48 ns | ~34 ns |
| `true` | ~155 ns | ~143 ns |
| `false` | ~89 ns | ~91 ns |
The `true` path improves because the override no longer re-emits the
full assertion-eval AST inline. The `false` (runtime-skip) path is
flat within noise — `should_evaluate?/2` short-circuits before the
closure is allocated.
### Fixed
- `Bond.CheckError`'s `message/1` no longer crashes when the error's
`:function` metadata is missing (regression introduced and fixed
internally during the `check` plumbing unification).
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.11.0] - 2026-05-21
0.11.0 reshapes the conditional-compilation config introduced in 0.10.0
around a new value space — `true | false | :purge` per kind — and adds two
new features that compose on top of it: runtime toggling without
recompilation, and per-module overrides.
### Breaking changes (minor)
- **`config :bond, <kind>: false` no longer compiles contracts out.** It
now means "compiled in, runtime guard defaults to off." If you used
`false` in 0.10.0 to get zero-overhead behaviour, change it to `:purge`
to preserve that behaviour. `true` continues to work as before (with the
addition of runtime toggleability — see below).
### Added
- **`:purge` mode for each contract kind.** Setting any of `:preconditions`,
`:postconditions`, or `:checks` to `:purge` causes Bond to emit no code
for that kind. The resulting BEAM contains no contract logic; per-call
overhead is zero. Contract documentation for that kind is also
suppressed.
- **Runtime toggling.** When a kind is compiled with `true` or `false`, the
emitted override carries a runtime guard:
`Application.get_env(:bond, <kind>, <compile_time_value>)`. The contract
is evaluated unless the runtime value is exactly `false`. Operators can
flip contracts on or off via `Application.put_env/3` from a remote
console — no recompilation needed. The compile-time value sets the
default for the runtime guard.
Benchmark on the project fixture (`bench/runtime_check_overhead.exs`,
trivial `@pre is_number(x)` in a tight loop): `:purge` ~48 ns/call,
`false` ~89 ns/call (~40 ns guard overhead), `true` ~155 ns/call (guard
plus assertion eval).
- **`:overrides` config for per-module rules.** A list of
`{Module | Regex, opts}` tuples. Module-atom keys match exactly; `Regex`
keys match against the source-visible module name (no `Elixir.` prefix).
Use this to opt specific modules in or out of contract compilation
without touching their source. Example:
config :bond,
preconditions: true,
overrides: [
{MyApp.HotPath, preconditions: :purge, postconditions: :purge},
{~r/Workers\\./, postconditions: false}
]
- **`use Bond, opts` per-module options.** Pass any of `:preconditions`,
`:postconditions`, `:checks` directly at the `use` site to override
global and `:overrides` settings for that module.
defmodule MyApp.HotPath do
use Bond, preconditions: :purge, postconditions: :purge
end
Precedence: `use Bond` opts > exact-atom `:overrides` match > first
`Regex` `:overrides` match > global config.
- **`Bond.Compiler.resolve_config/3`** — internal helper exposed for
testing that combines global config, `:overrides`, and `use Bond` opts
into the final per-module mode map.
### Changed
- `Bond.Compiler.AnnotatedFunction.apply_contract/2` now expects each kind
in the config map to be `true | false | :purge` rather than a boolean.
The function returns `nil` when both kinds resolve to `:purge`; in all
other cases it emits the override with the appropriate runtime guards.
- `Bond.check/1,2` now expands to a runtime-guarded call when the resolved
`:checks` mode is `true` or `false`, and to `:ok` (a compile-time no-op)
when the mode is `:purge`.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.10.0] - 2026-05-21
The headline feature of 0.10.0 is **conditional compilation** of contracts.
You can now compile some or all of your contracts out entirely via
application config, with zero per-call overhead for disabled contracts. The
release also adds an ExUnit helper module, polishes error reporting, and
substantially rewrites the user-facing documentation.
### Added
- **Conditional compilation via `:bond` application config.** Three keys,
read at compile time via `Application.compile_env/3`:
- `:preconditions` (default `true`) — when `false`, no precondition
evaluation is emitted in override clauses, and the auto-generated
`#### Preconditions` doc section is omitted.
- `:postconditions` (default `true`) — same for postconditions.
- `:checks` (default `true`) — when `false`, every `check/1,2` macro
call in modules that `use Bond` expands to `:ok` and the wrapped
expression is **not evaluated**. (Don't put side effects inside
`check`.)
When both `:preconditions` and `:postconditions` are disabled for a
function, Bond emits no override at all. The function runs exactly as
written, with zero per-call overhead. The function's auto-generated
contract docs are also suppressed in that case.
See the new "Conditional compilation" section in the `Bond` moduledoc.
- **`Bond.Test` module** with `assert_precondition_violation/2`,
`assert_postcondition_violation/2`, and `assert_check_violation/2`
macros for testing contract violations in ExUnit. Field expectations
(`:label`, `:expression`, etc.) can be exact values or `Regex` patterns.
- **New `guides/faq.md`** answering the questions that come up most: why
contracts when I have ExUnit, will contracts slow down prod, how does
Bond compare to Norm, what does Bond do that typespecs don't, the
Assertion Evaluation rule, default-arg behaviour, multi-clause handling.
### Changed
- **Assertion failure messages pretty-print the captured `binding/0`** with
`inspect/2 ... pretty: true, limit: 20, printable_limit: 200, width: 80`,
so small bindings stay compact and large structs no longer dominate the
failure output.
- **Stack traces of raised assertion exceptions are pruned** to omit
`Bond.*` frames. Failures point at the user's call site rather than
into `Bond.Runtime.Eval`.
- **`Bond` moduledoc / README restructured.** Leads with a five-line
`Account.withdraw` example and a one-paragraph elevator pitch. The
Wikipedia quote moves out. Assertion syntax recommends the keyword-list
form as primary. New `Conditional compilation` section. The `Math.sqrt`
example remains as the "showing everything" sample.
- **`guides/getting-started.md` expanded** into a step-by-step walkthrough:
first `@pre`, postcondition with `result`, labelled assertions,
predicates, `old` expressions, inline checks, disabling in prod, and
ExUnit integration.
### Internal
- New private function in `Bond.Runtime.Eval` that prunes Bond frames from
the captured stack trace before raising.
- `Bond.Compiler.AnnotatedFunction.apply_contract/1` is now
`apply_contract/2` taking a `contract_config` map.
The `__before_compile__/1` callback reads the config from a
`@__bond_contract_config__` module attribute set by Bond's `__using__/1`.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.9.1] - 2026-05-21
A patch release covering documentation cleanup left over from the 0.9.0
refactor plus a handful of usability improvements.
### Added
- `.formatter.exs` is now published with the Hex package and declares
`locals_without_parens` for `check/1`, `check/2`, and `old/1`. Downstream
projects can pick these up with `import_deps: [:bond]` in their own
`.formatter.exs`.
- Assertion-failure messages now include an `at: <file>:<line>` line so the
source location is clickable in editors.
### Changed
- `binding/0` captured in assertion-failure info is sorted by name so
failure messages are reproducible across runs.
### Fixed
- README/moduledoc no longer references the removed Bond.def/2 and
Bond.defp/2 macros, eliminating `mix docs` cross-reference warnings.
- The `getting-started` guide installation hint now references the current
version.
- CHANGELOG no longer auto-links the removed `define_function_with_contract/4`
helper.
### Internal
- Removed the vestigial `:context` field from `Bond.Compiler.Assertion`.
- Tightened the `Bond.Compiler.AnnotatedFunction` moduledoc.
## [0.9.0] - 2026-05-21
This release is a large internal refactor with no breaking changes to the
public API. `@pre`, `@post`, and `check/1,2` all behave the same as in 0.8.x.
### Changed
- **Bond no longer overrides `Kernel.def/2` and `Kernel.defp/2`.** Contracts
are now applied via Elixir compiler hooks (`@on_definition`,
`@before_compile`, `@after_compile`). This makes Bond more robust against
changes in Elixir's macro expansion semantics, eliminates a class of
macro-hygiene issues, and plays nicer with other macros that produce
function definitions.
- **Multi-clause functions are now wrapped by a single override clause that
delegates to `super/1`** rather than having contract logic inlined into
each clause. Elixir's normal pattern matching handles dispatch inside the
`super` call.
- **Assertion failures are signalled by a throw / catch** instead of being
raised inline. Each `@pre`/`@post` group compiles to an anonymous function
that throws `{:assertion_failure, info}` on the first failure;
`Bond.Runtime.Eval` catches it and raises the appropriate exception type.
- Functions with contracts now get auto-generated `Preconditions` and
`Postconditions` sections in their documentation even if the user did not
attach a `@doc` themselves. Previously contract documentation was only
emitted when a `@doc` was present.
- Internal modules are reorganised into `Bond.Compiler.*` (compile-time) and
`Bond.Runtime.*` (run-time) namespaces.
### Internal
- New modules: `Bond.Compiler.AnnotatedFunction` (multi-clause function
model), `Bond.Compiler.FunctionDefinition`, `Bond.Compiler.CompileStateFSM`
(rewritten), `Bond.Runtime.Eval`.
- Removed internal modules `Bond.Compiler.AnnotatedFunctionClause` and
`Bond.Compiler.LegacyCompileStateFSM`, along with the
`define_function_with_contract/4` helper they used.
- `Bond.Compiler.Assertion` now carries a stable random `:id` for use in
error reporting and future internal tooling.
### Requirements
- Unchanged. Elixir `~> 1.14`.
## [0.8.3] - 2024-11-08
Released before this changelog was established. See the git history for
details.