View Source Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[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.stackin@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__{} = nameor has anis_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__{...} = nameto enable the pre-check.- On entry, when the function head pattern-matches
Bond.InvariantError— new exception parallel toPreconditionError/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.:invariantsconditional-compilation key. Joins:preconditions,:postconditions, and:checks. Sametrue | false | :purgevalue space; same runtime toggleability viaApplication.put_env/3; same:overridesanduse 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 fromBond.Compiler.AnnotatedFunctionfor 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 => :invariantin the metadata. No subscriber changes are needed — existing handlers attached to the event automatically pick up the new kind.The internal
Bond.Compiler.Assertionstruct gains a:binding_namefield, populated only on:invariantassertions from the declaration's first argument.Bond.Compiler.AnnotatedFunctiongains an:invariantsfield plusput_invariants/2andhas_invariants?/1helpers.override?/1widens to emit overrides for public functions in modules with@invariants, even when the function has no per-function@pre/@post.Bond.Compiler.CompileStateFSMtracks 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, orcheck— immediately before the correspondingBond.PreconditionError/Bond.PostconditionError/Bond.CheckErroris raised. Single event family for all three kinds; consumers filter on the:kindmetadata. Measurements carry:system_timeand:monotonic_time; metadata carries:kind,:module,:function,:label,:expression,:assertion_id,:file,:line, and:binding. See the new "Telemetry" section in theBondmoduledoc / README.{:telemetry, "~> 1.0"}is now a regular dependency.Bond.Runtime.Eval.should_evaluate?/2— internal helper that performs theApplication.get_env/3runtime 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
defps 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 viaBond.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 callsshould_evaluate?(:preconditions, <compile_time_mode>)and only builds the assertion-evaluation closure when that returnstrue. TheApplication.get_env/3lookup logic lives entirely inBond.Runtimerather than being inlined at every contract'd function.Bond.check/1,2routes through the same throw/catch path as@pre/@post. All three kinds now produce{:assertion_failure, info}throws caught byBond.Runtime.Eval, which fires the telemetry event and raises. This unifies the plumbing across the three kinds; previouslycheckraised 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
truepath improves because the override no longer re-emits the full assertion-eval AST inline. Thefalse(runtime-skip) path is flat within noise —should_evaluate?/2short-circuits before the closure is allocated.
Fixed
Bond.CheckError'smessage/1no longer crashes when the error's:functionmetadata is missing (regression introduced and fixed internally during thecheckplumbing 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>: falseno longer compiles contracts out. It now means "compiled in, runtime guard defaults to off." If you usedfalsein 0.10.0 to get zero-overhead behaviour, change it to:purgeto preserve that behaviour.truecontinues to work as before (with the addition of runtime toggleability — see below).
Added
:purgemode for each contract kind. Setting any of:preconditions,:postconditions, or:checksto:purgecauses 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
trueorfalse, the emitted override carries a runtime guard:Application.get_env(:bond, <kind>, <compile_time_value>). The contract is evaluated unless the runtime value is exactlyfalse. Operators can flip contracts on or off viaApplication.put_env/3from 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).:overridesconfig for per-module rules. A list of{Module | Regex, opts}tuples. Module-atom keys match exactly;Regexkeys match against the source-visible module name (noElixir.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, optsper-module options. Pass any of:preconditions,:postconditions,:checksdirectly at theusesite to override global and:overridessettings for that module.defmodule MyApp.HotPath do use Bond, preconditions: :purge, postconditions: :purge endPrecedence:
use Bondopts > exact-atom:overridesmatch > firstRegex:overridesmatch > global config.Bond.Compiler.resolve_config/3— internal helper exposed for testing that combines global config,:overrides, anduse Bondopts into the final per-module mode map.
Changed
Bond.Compiler.AnnotatedFunction.apply_contract/2now expects each kind in the config map to betrue | false | :purgerather than a boolean. The function returnsnilwhen both kinds resolve to:purge; in all other cases it emits the override with the appropriate runtime guards.Bond.check/1,2now expands to a runtime-guarded call when the resolved:checksmode istrueorfalse, 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
:bondapplication config. Three keys, read at compile time viaApplication.compile_env/3::preconditions(defaulttrue) — whenfalse, no precondition evaluation is emitted in override clauses, and the auto-generated#### Preconditionsdoc section is omitted.:postconditions(defaulttrue) — same for postconditions.:checks(defaulttrue) — whenfalse, everycheck/1,2macro call in modules thatuse Bondexpands to:okand the wrapped expression is not evaluated. (Don't put side effects insidecheck.)
When both
:preconditionsand:postconditionsare 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
Bondmoduledoc.Bond.Testmodule withassert_precondition_violation/2,assert_postcondition_violation/2, andassert_check_violation/2macros for testing contract violations in ExUnit. Field expectations (:label,:expression, etc.) can be exact values orRegexpatterns.New
guides/faq.mdanswering 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/0withinspect/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 intoBond.Runtime.Eval.Bondmoduledoc / README restructured. Leads with a five-lineAccount.withdrawexample and a one-paragraph elevator pitch. The Wikipedia quote moves out. Assertion syntax recommends the keyword-list form as primary. NewConditional compilationsection. TheMath.sqrtexample remains as the "showing everything" sample.guides/getting-started.mdexpanded into a step-by-step walkthrough: first@pre, postcondition withresult, labelled assertions, predicates,oldexpressions, inline checks, disabling in prod, and ExUnit integration.
Internal
- New private function in
Bond.Runtime.Evalthat prunes Bond frames from the captured stack trace before raising. Bond.Compiler.AnnotatedFunction.apply_contract/1is nowapply_contract/2taking acontract_configmap. The__before_compile__/1callback 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.exsis now published with the Hex package and declareslocals_without_parensforcheck/1,check/2, andold/1. Downstream projects can pick these up withimport_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/0captured 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 docscross-reference warnings. - The
getting-startedguide installation hint now references the current version. - CHANGELOG no longer auto-links the removed
define_function_with_contract/4helper.
Internal
- Removed the vestigial
:contextfield fromBond.Compiler.Assertion. - Tightened the
Bond.Compiler.AnnotatedFunctionmoduledoc.
[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/2andKernel.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/1rather than having contract logic inlined into each clause. Elixir's normal pattern matching handles dispatch inside thesupercall. - Assertion failures are signalled by a throw / catch instead of being
raised inline. Each
@pre/@postgroup compiles to an anonymous function that throws{:assertion_failure, info}on the first failure;Bond.Runtime.Evalcatches it and raises the appropriate exception type. - Functions with contracts now get auto-generated
PreconditionsandPostconditionssections in their documentation even if the user did not attach a@docthemselves. Previously contract documentation was only emitted when a@docwas present. - Internal modules are reorganised into
Bond.Compiler.*(compile-time) andBond.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.AnnotatedFunctionClauseandBond.Compiler.LegacyCompileStateFSM, along with thedefine_function_with_contract/4helper they used. Bond.Compiler.Assertionnow carries a stable random:idfor 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.