Bond (design by contract for Elixir) - 1.0.0-rc.1 released

A little over 18 months ago I posted the original announcement for Bond — an
early cut of a Design by Contract library for Elixir. The thread surfaced excellent feedback (thanks again to @sbuttgereit, @dimitarvp, @katafrakt, @zachallaun, @jarlah, and @Asd), much of which shaped where the library went next.

Today I’m announcing Bond 1.0.0-rc.1, the first release candidate on the road to 1.0.0.

Install

def deps do
  [{:bond, "~> 1.0.0-rc.1"}]
end

What’s landed since the original announcement

The things people asked for in the original thread — and a lot more.

  • Invariants (@invariant) for struct modules. The subject binding refers to the struct instance under check; Bond detects the struct parameter in each public function’s head and threads subject through automatically. This was the most-requested missing piece in the 2024 thread.

  • A “Contracts in a Concurrent World” guide that takes the dimitarvp / katafrakt / zachallaun discussion further: how old/1 interacts with state owned by other processes, the locking pattern for Agent/GenServer/ETS-backed state, and why pure functions are still the easiest place to apply contracts.

  • Conditional compilation per environment. Each contract kind (:preconditions, :postconditions, :invariants, :checks) takes true, false, or :purge. :purge strips the contract code at compile time — zero runtime overhead, the contract isn’t there. true / false are also runtime-togglable via Application.put_env/3. Per-module overrides are supported.

  • Compatibility with other libraries that override Kernel.@/1. Bond uses the same technique as Norm. Combining them in the same module fails to compile with a clear ambiguity error rather than silently letting one win; the FAQ documents the workaround (split modules). For the broader def/defp compatibility concern @Asd raised, Bond’s compiler architecture is built on @on_definition / @before_compile / @after_compile plus defoverridable at end-of-module — it does not redefine def or defp.

  • Multi-clause contracts clarified and documented. @pre / @post attach to the next def and apply across all its clauses; the per-clause story is intentionally out of scope for 1.0 and called out as a deliberate boundary in the FAQ.

  • Predicate typespecs corrected to use as_boolean(...) instead of boolean(), so Dialyzer no longer rejects truthy non-boolean values that Bond’s predicates accept.

What 1.0 brings on top of that

The RC adds the things you want from a 1.0:

  • A documented and frozen public API surface. Every name covered by the SemVer contract is enumerated in guides/public-api.md — module attributes, macros, operators, predicates, Bond.Test and Bond.PropertyTest helpers, telemetry events, error structs, config keys, types. Internal namespaces (Bond.Compiler.*, Bond.Runtime.*) are explicitly carved out.

  • A SemVer stability promise. guides/stability.md spells out what patch / minor / major actually mean for Bond, what’s explicitly excluded (compile-error message text, generated-code shape, exception message text), and the deprecation policy (minimum one minor with a warning before removal in next major).

  • Published overhead numbers. guides/overhead.md documents both compile-time and runtime cost from a documented reference environment (M3 Max, OTP 27.2, Elixir 1.19.5), with mix run bench/... recipes for re-running on your hardware. Headlines: a :purged contract is free; an enabled @pre adds ~130 ns/call; Bond’s compile-time overhead is ~10 ms per module that uses contracts.

  • :warn_skipped_invariants — a new opt-out compile warning that catches the most common silent-skip footgun (an invariant-declaring module with a public function that neither matches the struct nor returns one). Three suppression scopes are provided: per-function attribute, per-module use option, global config.

  • Compatibility verified across Elixir 1.16–1.19 in CI, with parallel-compile races resolved and a Dialyzer baseline for Bond’s own code.

Why a release candidate, not 1.0.0 final?

The stability guarantees in guides/stability.md lock in at 1.0.0 final, not at RC. The RC window is explicitly for soliciting feedback from the Elixir community on the public API surface before that ink dries. If you find rough edges in the docs, surprising naming choices, missing predicates, awkward composition with other libraries, or anything that smells wrong for a 1.0, please open an issue or reply here — small adjustments to the public surface are still on the table between RC and final.

Bond intentionally stays in its lane: it doesn’t try to replace typespecs, set-theoretic types, or ExUnit. It complements them with runtime-checked, parameter-and-result-and-state assertions, written next to the code they constrain, with errors that tell you exactly what failed and why.

Bug reports and feedback welcome at Issues · jvoegele/bond · GitHub, or in reply here.

Links

16 Likes

Bond 1.0.0-rc.2 is out — the second release candidate on the road to 1.0.0.

def deps do
  [{:bond, "~> 1.0.0-rc.2"}]
end

This RC is small and focused. The headline is improved compatibility: Bond can now share a single module with other libraries that override Kernel.@/1 or wrap your functions — Norm being the obvious one. In rc.1 the answer was “split them into separate modules”; now they can live together.

What changed since rc.1

  • Same-module coexistence, part 1 — use Bond, at_annotations: false. This opts a module out of Bond’s Kernel.@/1 override, so another library (e.g. Norm’s @contract) can own @ in that module. You then write contracts as the fully-qualified Bond.pre/1, Bond.post/1, and Bond.invariant/1 calls (check/1 stays as-is). The bare pre/post/invariant macros are never imported in either mode, so they can’t collide with your function names. The FAQ has a worked “Bond + Norm in the same module” example.

  • Same-module coexistence, part 2 — tolerance of externally-generated override clauses. Libraries that wrap a function by making it defoverridable and redefining it (Norm’s @contract, anything built on the decorator library, …) inject a clause that Bond’s @on_definition hook observes. Bond used to reject that as the function being defined twice. It now recognizes those generated wrappers, ignores them for its bookkeeping, and still wraps the function as a whole — composing with the other library’s wrapper via super.

  • Fix: the ElixirLS / IEx already_started crash. If you were dogfooding rc.1 in an editor, you may have hit a (MatchError) … {:error, {:already_started, #PID<…>}} on essentially every edit of a use Bond module. An aborted compile (a transient syntax error while you’re typing) could leave Bond’s per-module compile-state process registered in the long-lived BEAM, and the next compile crashed trying to start it again. Bond now discards the stale process and starts fresh. One-shot mix compile was never affected — this only bit editor/IEx sessions that keep the VM alive across recompiles. (Found by dogfooding Bond in a real app; exactly the kind of rough edge the RC window is for.)

One breaking change to be aware of

If you tried out rc.1 and used the positional @pre / @post label forms, they’ve been removed:

# Was (removed in rc.2):
@pre :positive, x > 0
@post result >= 0, "non-negative result"

# Now — labels are a keyword key (quote it for spaces/punctuation):
@pre positive: x > 0
@post "non-negative result": result >= 0

@pre expr (bare) and @pre label: expr (keyword) are the two remaining forms; the qualified Bond.pre/Bond.post calls are likewise keyword-only. The removed shapes raise a CompileError with the migration message, so you won’t get a silent change in behavior. This finishes unifying contract labelling on the single keyword-list form that check/2 already moved to in 0.16.0 — RC is the right time to settle it before the public surface freezes at 1.0.0 final.

Still an RC

Everything from the rc.1 announcement still stands — the frozen public API surface, the SemVer stability promise, the published overhead numbers, and 1.16–1.19 CI coverage. The stability guarantees lock in at 1.0.0 final, not at RC, so feedback on naming, composition, docs, or anything that smells wrong for a 1.0 is still very welcome here or at Issues · jvoegele/bond · GitHub.

1 Like

This sounds like a very interesting library, and it totally flew under the radar for me.

I shall now put it on my teetering pile of “things I really want to use”. Seems like it could be useful enough to stay near the top…

1 Like