Packages
bond
0.1.0
1.13.0
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.0
1.4.0
1.3.0
1.3.0-rc.1
1.2.1
1.2.0
1.1.0
1.0.0
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
0.18.0
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.1.0
Design by Contract (DbC) for Elixir
Current section
Files
Jump to
Current section
Files
lib/bond.ex
defmodule Bond do
@moduledoc """
Design By Contract for Elixir.
As described on [Wikipedia](https://en.wikipedia.org/wiki/Design_by_contract):
> Design by contract (DbC), also known as contract programming, programming by contract and
> design-by-contract programming, is an approach for designing software.
>
> It prescribes that software designers should define formal, precise and verifiable interface
> specifications for software components, which extend the ordinary definition of
> [abstract data types](https://en.wikipedia.org/wiki/Abstract_data_type) with preconditions,
> postconditions and invariants. These specifications are referred to as "contracts", in
> accordance with a conceptual metaphor with the conditions and obligations of business
> contracts.
>
> The term was coined by [Bertrand Meyer](https://en.wikipedia.org/wiki/Bertrand_Meyer) in
> connection with his design of the
> [Eiffel programming language](https://en.wikipedia.org/wiki/Eiffel_(programming_language))
> and first described in various articles starting in 1986 and the two successive editions
> (1988, 1997) of his book
> [_Object-Oriented Software Construction_](https://en.wikipedia.org/wiki/Object-Oriented_Software_Construction).
>
> Design by contract has its roots in work on
> [formal verification](https://en.wikipedia.org/wiki/Formal_verification),
> [formal specification](https://en.wikipedia.org/wiki/Formal_specification) and
> [Hoare logic](https://en.wikipedia.org/wiki/Hoare_logic).
`Bond` applies the central ideas of contract programming to Elixir and provides support for
attaching preconditions and postconditions to function definitions and conditionally evaluating
them based on compile-time configuration.
## Quick start
`use Bond` in your module and then define preconditions and postconditions for your functions
with the `@pre` and `@post` annotations, respectively. For example:
```elixir
defmodule Math do
use Bond
@pre numeric_x: is_number(x), non_negative_x: x >= 0
@post float_result: is_float(result),
non_negative_result: result >= 0.0,
"sqrt of 0 is 0": (x == 0) ~> (result === 0.0),
"sqrt of 1 is 1": (x == 1) ~> (result === 1.0),
"x > 1 implies result smaller than x": (x > 1) ~> (result < x)
def sqrt(x), do: :math.sqrt(x)
end
```
## Usage
> #### `use Bond` {: .info}
>
> When you `use Bond`, the `Bond` module will override several `Kernel` macros in order to
> support attaching preconditions and postconditions to functions. Specifically:
>
> * `Kernel.@/1` is overridden by `Bond.@/1`
> * `Kernel.def/2` is overridden by `Bond.def/2`
> * `Kernel.defp/2` is overridden by `Bond.defp/2`
>
> `use Bond` will also import the `Bond` module so that the `check/1` and `check/2` macros are
> available for use.
>
> Additionally, the `Bond.Predicates` module is automatically imported for all preconditions,
> postconditions, and checks, so that the predicate functions and operators that are defined
> therein can be used for assertions. `Bond.Predicates` can be explicitly imported into modules
> for use outside of assertions.
## Assertion syntax
Assertions in Bond are conditional Elixir expressions, optionally associated with a textual
label (either an atom or a string). These assertions may appear in `@pre` or `@post`
expressions, or in calls to `check/1` or `check/2`.
Bond offers considerable flexibility in its assertion syntax; assertions may take any of the
following forms:
* `expression` - a "bare" expression without any associated label
* `label, expression` - an expression preceded by a string or atom label
* `expression, label` - an expression followed by a string or atom label
* `label_1: expression_1, label_2: expression_2` - a keyword list with labels as the keys and
expressions as the associated values
"""
alias Bond.Assertion
alias Bond.CompileStateFSM, as: FSM
alias Bond.OldExpression
@typedoc false
@type assertion_kind :: :precondition | :postcondition | :check
@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()}
@typedoc """
Subset of `Macro.Env` struct that excludes fields that, according to the documentation, "are
private to Elixir's macro expansion mechanism".
"""
@type env :: %{
optional(:__struct__) => module(),
context: Macro.Env.context(),
context_modules: Macro.Env.context_modules(),
file: Macro.Env.file(),
function: Macro.Env.name_arity() | nil,
line: Macro.Env.line(),
module: module()
}
@doc false
defmacro __using__(_opts) do
module = __CALLER__.module
{:ok, fsm_pid} = FSM.start_link(module)
Module.put_attribute(module, :_bond_fsm_pid, fsm_pid)
quote do
import Kernel, except: [@: 1, def: 2, defp: 2]
import Bond
@before_compile Bond
end
end
@doc false
defmacro __before_compile__(%Macro.Env{} = env) do
FSM.stop(fsm(env))
Module.delete_attribute(env.module, :_bond_fsm_pid)
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
defmacro @{pre_or_post, meta, [expression]} when pre_or_post in [:pre, :post] do
# This clause handles "bare" @pre or @post assertions that either do not have a label
# attached to them, or a keyword list where the keys are labels and the values are the
# assertions.
if Keyword.keyword?(expression) do
for {label, expression} <- expression do
register_assertion(pre_or_post, expression, label, __CALLER__, meta)
end
else
register_assertion(pre_or_post, expression, nil, __CALLER__, meta)
end
end
defmacro @{pre_or_post, meta, [label, {_, _, _} = expression]}
when (pre_or_post in [:pre, :post] and is_atom(label)) or is_binary(label) do
# This clause handles @pre or @post assertions that have a label preceding them.
register_assertion(pre_or_post, expression, label, __CALLER__, meta)
end
defmacro @{pre_or_post, meta, [{_, _, _} = expression, label]}
when (pre_or_post in [:pre, :post] and is_atom(label)) or is_binary(label) do
# This clause handles @pre or @post assertions that have a label following them.
register_assertion(pre_or_post, expression, label, __CALLER__, meta)
end
defmacro @attr do
# Forward any other module attributes that are not `@pre` or `@post` to `Kernel.@/1`
quote do
Kernel.@(unquote(attr))
end
end
@doc """
Override `Kernel.def/2` to support wrapping with preconditions and postconditions.
"""
defmacro def(definition, body) do
define_function_with_contract(__CALLER__, definition, body, true)
end
@doc """
Override `Kernel.defp/2` to support wrapping with preconditions and postconditions.
"""
defmacro defp(definition, body) do
define_function_with_contract(__CALLER__, definition, body, false)
end
@doc """
Check an assertion or a keyword list of assertions for validity.
Returns the result(s) of the assertion(s) if satisfied, or raises a `Bond.CheckError` exception
if any assertions are not satisfied.
## Examples
iex> check 1 == 1.0
true
iex> check 1 == 1.0, "integer 1 is equal to float 1.0"
true
iex> check "integer 1 is equal to float 1.0", 1 == 1.0
true
iex> check tautology: 1 == 1
[true]
iex> check "1 is 1": 1 == 1, "2 is 2": 2 == 2
[true, true]
"""
@spec check(assertion_expression()) :: as_boolean(any())
@spec check(Keyword.t(assertion_expression())) :: list(as_boolean(any()))
defmacro check(assertion_or_list_of_assertions)
defmacro check(keyword_list) when is_list(keyword_list) do
for {label, {_, meta, _} = expression} <- keyword_list do
check_assertion(expression, label, __CALLER__, meta)
end
end
defmacro check({_, meta, _} = expression) do
check_assertion(expression, nil, __CALLER__, meta)
end
@doc """
Check a single labelled assertion for validity.
See `check/1` for details and examples.
"""
defmacro check(label_or_expression, expression_or_label)
@spec check(assertion_label(), assertion_expression()) :: as_boolean(any())
defmacro check(label, {_, meta, _} = expression) when is_atom(label) or is_binary(label) do
check_assertion(expression, label, __CALLER__, meta)
end
@spec check(assertion_expression(), assertion_label()) :: as_boolean(any())
defmacro check({_, meta, _} = expression, label) when is_atom(label) or is_binary(label) do
check_assertion(expression, label, __CALLER__, meta)
end
defp fsm(%Macro.Env{module: module}), do: Module.get_attribute(module, :_bond_fsm_pid)
defp check_assertion(expression, label, env, meta) do
check = Bond.Assertion.new(:check, label, expression, env, meta)
Bond.Assertion.quoted_eval(check)
end
defp register_assertion(:pre, expression, label, env, meta) do
register_assertion(:precondition, expression, label, env, meta)
end
defp register_assertion(:post, expression, label, env, meta) do
register_assertion(:postcondition, expression, label, env, meta)
end
defp register_assertion(kind, expression, label, env, meta) do
assertion = Assertion.new(kind, label, expression, env, meta)
fsm_event =
case kind do
:precondition -> :precondition_def
:postcondition -> :postcondition_def
end
apply(FSM, fsm_event, [fsm(env), assertion])
end
defp define_function_with_contract(env, definition, body, public?) do
fsm = fsm(env)
FSM.function_def(fsm, definition)
preconditions = FSM.pending_preconditions(fsm)
postconditions = FSM.pending_postconditions(fsm)
body_with_contracts = wrap_function_body(body, preconditions, postconditions)
if public? do
quote do
Kernel.def(unquote(definition), unquote(body_with_contracts))
end
else
quote do
Kernel.defp(unquote(definition), unquote(body_with_contracts))
end
end
end
defp wrap_function_body(body, preconditions, postconditions) do
preconditions_ast = Enum.map(preconditions, &Assertion.quoted_eval/1)
{postconditions, old_context} = OldExpression.precompile(postconditions)
old_resolved_ast = OldExpression.resolve(old_context)
postconditions_ast = Enum.map(postconditions, &Assertion.quoted_eval(&1))
Keyword.update!(body, :do, fn do_block ->
quote do
unquote_splicing(preconditions_ast)
unquote_splicing(old_resolved_ast)
var!(result) = unquote(do_block)
unquote_splicing(postconditions_ast)
var!(result)
end
end)
end
end