defmodule Bond do # Pull in the moduledocs from the demarcated section of the README file @readme Path.expand("./README.md") @external_resource @readme @moduledoc @readme |> File.read!() |> String.split("") |> Enum.at(1) |> String.split("") |> List.first() # `require` (not `alias`) so Mix creates strong compile-time deps on both # Bond.Compiler and Bond.Compiler.AnnotatedFunction, and schedules both # before this file. Every user module has a compile dep on bond.ex via # `use Bond`, so this transitively ensures: # # (a) Bond.Compiler.AnnotatedFunction is compiled before any user # module's @on_definition callbacks fire (the original 0.17.4 race # fix, see the bond-compile-order memory note). # (b) The full chain Bond.Compiler → CompileStateFSM → Server is on # disk before this file's __using__ macro body calls # `Bond.Compiler.init/1` (which starts the gen_statem). # # (b) is needed because the call to Bond.Compiler.init/1 from the macro # body would otherwise be just a fully-qualified reference — sometimes # tracked as a compile dep by Mix's parallel scheduler, sometimes not. # Under cache-warm CI conditions, a doc-only change to server.ex was # enough to flip a previously-working race and break compilation of # `use Bond` modules — see the gotcha note for the symptom + diagnosis. require Bond.Compiler require Bond.Compiler.AnnotatedFunction @typedoc false @type assertion_kind :: :precondition | :postcondition | :check | :invariant @typedoc """ Type to represent a label for an assertion, which must be a compile-time atom or string. """ @type assertion_label :: String.t() | atom() @typedoc """ Type to represent a compile-time quoted assertion expression, which must be a valid Elixir expression that, when unquoted, evaluates to a `t:boolean/0` or `t:as_boolean/1` value. """ @type assertion_expression :: {atom(), Macro.metadata(), list()} @doc """ `use Bond` enables `@pre`, `@post`, and `check/1` annotations in the using module. When the module also inherits contracts (`use Bond, behaviours: […]`), `@pre_weaken` and `@post_strengthen` are additionally available to *refine* an inherited contract — weakening a precondition and strengthening a postcondition respectively, per Eiffel's behavioural-subtyping rules. See `Bond.Behaviour`. ## Options Each of the following options is one of `true`, `false`, or `:purge`. See the "Conditional compilation" section in the moduledoc for what each value means. Options passed to `use Bond` override both the global `:bond` config and any `:overrides` entry that matches this module. * `:preconditions` — mode for this module's `@pre` annotations. * `:postconditions` — mode for this module's `@post` annotations. * `:checks` — mode for this module's `check/1` calls. * `:invariants` — mode for this module's `@invariant` annotations. Example: a hot-path module that wants contracts purged from its compiled output regardless of the global config. defmodule MyApp.HotPath do use Bond, preconditions: :purge, postconditions: :purge end ### `:at_annotations` Controls Bond's `@`-prefixed annotation syntax — `@pre`, `@post`, and `@invariant`. By default (`true`) Bond overrides `Kernel.@/1` in the using module so those forms are recognised. Overriding `@` is lexically scoped to this module, so it is invisible to the rest of your project — but it cannot coexist *within a single module* with another library that also overrides `@` (for example Norm's `@contract`). Pass `at_annotations: false` to leave `Kernel.@/1` untouched in this module. Bond's compiler hooks are still installed, but the `@pre`/`@post`/`@invariant` forms are **not** available; instead, write contracts as fully-qualified calls — `Bond.pre/1`, `Bond.post/1`, and `Bond.invariant/1`. `check/1` remains available unqualified. defmodule MyApp.Validated do use Norm use Bond, at_annotations: false @contract add(integer(), integer()) :: integer() Bond.pre x >= 0 and y >= 0 Bond.post result >= 0 def add(x, y), do: x + y end > #### Bare macros are always fully-qualified {: .info} > > The `pre`/`post`/`invariant` macros are never imported, even with the default > `at_annotations: true`. This keeps them from colliding with common function names (notably > `post`) in modules that only ever use the `@` forms. They are reachable only as > `Bond.pre`, `Bond.post`, and `Bond.invariant`. """ defmacro __using__(opts) when is_list(opts) do Bond.Compiler.init(__CALLER__.module) {at_annotations?, opts_without_at} = Keyword.pop(opts, :at_annotations, true) # Resolve the `:behaviours` option's module references in the caller's context (they arrive # as unresolved `__aliases__` AST) and register their inherited contracts with this module's # FSM. The call to each behaviour's `__bond_contracts__/0` establishes the compile-time # dependency that forces the behaviour to be compiled first. {behaviours_opt, use_opts} = Keyword.pop(opts_without_at, :behaviours, []) behaviour_mods = behaviours_opt |> List.wrap() |> Enum.map(&Macro.expand(&1, __CALLER__)) Bond.Compiler.register_behaviours(__CALLER__.module, behaviour_mods, __CALLER__) behaviours_ast = for behaviour <- behaviour_mods do quote do @behaviour unquote(behaviour) end end config_ast = quote do # Read the `:bond` application config in the *user's* module body so # `Application.compile_env/3` works (it cannot be called inside a macro/function body, # only in a module body) and so the compile-env dependency is correctly tracked for # recompilation. `Bond.Compiler.resolve_config/3` merges global config, `:overrides`, # and the `use Bond` opts. `Bond.Compiler.__before_compile__/1` reads the final # `@__bond_contract_config__` attribute when emitting contract overrides. @__bond_contract_config__ Bond.Compiler.resolve_config( __MODULE__, unquote(use_opts), preconditions: Application.compile_env(:bond, :preconditions, true), postconditions: Application.compile_env(:bond, :postconditions, true), checks: Application.compile_env(:bond, :checks, true), invariants: Application.compile_env(:bond, :invariants, true), overrides: Application.compile_env(:bond, :overrides, []), warn_skipped_invariants: Application.compile_env( :bond, :warn_skipped_invariants, true ) ) end # `at_annotations: true` (default): shadow `Kernel.@/1` with Bond's `@/1` and import only # the `@` macro plus `check`. The bare `pre`/`post`/`invariant` macros are deliberately # left out of the import list so they never collide with user function names. # `at_annotations: false`: leave `@` alone (so libraries like Norm can own it), import only # `check`, and rely on fully-qualified `Bond.pre`/`Bond.post`/`Bond.invariant` calls. imports_ast = if at_annotations? do quote do import Kernel, except: [@: 1] import Bond, only: [@: 1, check: 1, check: 2, defcontract: 1, defcontract: 2] end else quote do import Bond, only: [check: 1, check: 2, defcontract: 1, defcontract: 2] end end hooks_ast = quote do @on_definition Bond.Compiler @before_compile Bond.Compiler @after_compile Bond.Compiler end quote do unquote(config_ast) unquote(imports_ast) unquote_splicing(behaviours_ast) unquote(hooks_ast) end end @doc """ Override `Kernel.@/1` to support `@pre` and `@post` annotations. See the `Bond` module docs for the syntax of `@pre` and `@post` annotations. """ defmacro @pre_or_post # This clause handles either "bare" @pre or @post assertions that do not have a label # attached to them, or keyword lists where the keys are labels and the values are the # assertions. defmacro @{pre_or_post, meta, [expression]} when pre_or_post in [:pre, :post] do register_pre_or_post(pre_or_post, expression, __CALLER__, meta) end # `@pre_weaken` / `@post_strengthen` — Eiffel-style refinement of a contract inherited from a # `Bond.Behaviour` callback or `Bond.Protocol` function (#16). Same single-arg / keyword-list # shape as `@pre`/`@post`; the assertion is registered tagged with its refinement role so the # inheritance merge *folds* it (precondition weakened with `or`, postcondition strengthened with # `and`) instead of rejecting it. Plain `@pre`/`@post` on an inherited operation stays a compile # error. Refinement expressions reference the abstraction's canonical argument names (the # callback's or protocol function's), the same vocabulary as the inherited contract. defmacro @{refinement, meta, [expression]} when refinement in [:pre_weaken, :post_strengthen] do register_refinement(refinement, expression, __CALLER__, meta) end # The positional label forms `@pre