Packages
bond
1.10.1
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/compiler/old_expression.ex
defmodule Bond.Compiler.OldExpression do
@moduledoc internal: true
@moduledoc """
Support for the `old` construct that can be used in postconditions for capturing values of
expressions prior to execution of a function.
"""
alias Bond.Compiler.Assertion
@opaque old_context :: %{String.t() => Bond.assertion_expression()} | %{}
@doc """
Transforms `postconditions` such that any `old` expressions are in resolvable form.
The `postconditions` argument must be a list of `%Bond.Compiler.Assertion{kind: :postcondition}`
structs, since old expressions are valid only in postconditions.
Returns a tuple containing the transformed postconditions and an opaque value that can be
provided to `resolve/1` for resolving old expressions to their runtime value prior to function
execution.
"""
@spec precompile([%Bond.Compiler.Assertion{kind: :postcondition}]) :: {Macro.t(), old_context()}
def precompile(postconditions) when is_list(postconditions) do
{precompiled_postconditions, old_table} =
Enum.reduce(postconditions, {[], %{}}, fn postcondition, acc ->
accumulate_old_expressions(postcondition, acc)
end)
{Enum.reverse(precompiled_postconditions), old_table}
end
@doc """
Resolves old expressions in the given `old_context` to their runtime values.
The runtime value of each old expression is a snapshot obtained by unquoting the expression
within the body of the associated function, just prior to executing the function body. These
snapshot values are saved in local variables, which are then accessed when evaluating the
postconditions after the function has finished its normal execution. Note that these local
variables are not hygienized and therefore are accessible within the body of the function
associated with the postconditions, and will also appear in the `binding/0` of the function.
Returns a list of quoted expressions that, when evaluated with `unquote_splicing/1`, inject
local variables (containing snapshot values for the old expressions) into the calling context.
"""
@spec resolve(old_context()) :: term()
def resolve(empty_map) when empty_map == %{}, do: []
def resolve(old_context) do
for {var, expression} <- pairs(old_context) do
quote generated: true do
var!(unquote(var)) = unquote(expression)
end
end
end
@doc """
Returns a stable-ordered list of `{var, expression}` tuples for the given `old_context`.
The variable AST is the same one `resolve/1` introduces into the calling scope. Used by
`Bond.Compiler.AnnotatedFunction` to derive consistent forwarding-args and parameter-pattern
lists for the generated postcondition defp, so the order of old-value parameters matches the
order of the assignments produced by `resolve/1`.
"""
@spec pairs(old_context()) :: [{Macro.t(), Bond.assertion_expression()}]
def pairs(empty_map) when empty_map == %{}, do: []
def pairs(old_context) do
old_context
|> Enum.sort_by(fn {key, _} -> key end)
|> Enum.map(fn {key, expression} -> {make_var(key), expression} end)
end
defp make_key(old_expression), do: Macro.to_string(old_expression)
defp make_var(key), do: Macro.var(String.to_atom("old(#{key})"), nil)
defp accumulate_old_expressions(
%Assertion{kind: :postcondition, expression: expression} = assertion,
{postconditions_acc, old_table}
) do
{updated_expression, updated_old_table} =
Macro.prewalk(expression, old_table, fn
{:old, _meta, [old_expression]}, old_table ->
key = make_key(old_expression)
var = make_var(key)
{var, Map.put(old_table, key, old_expression)}
{:old, _, _}, _old_table_acc ->
raise CompileError, description: "invalid old expression"
other, old_table ->
{other, old_table}
end)
updated_assertion = %{assertion | expression: updated_expression}
{[updated_assertion | postconditions_acc], updated_old_table}
end
end