Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: do not redefine def macro and use attribute tags #1

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 78 additions & 21 deletions lib/contracts.ex
Original file line number Diff line number Diff line change
@@ -1,42 +1,99 @@
defmodule Contracts do
@default %{pre: true, post: true}
defmodule Contract do
defstruct precondition: nil, postcondition: nil, func_name: nil, func_args: nil, func_guards: nil, func_body: nil
end

defmacro __using__(_opts) do
{:ok, _} = Agent.start_link(fn -> @default end, name: __name__(__CALLER__))
quote do
import Kernel, except: [def: 2]
import Contracts
@before_compile unquote(__MODULE__)

Module.register_attribute(__MODULE__, :contract_predicates, accumulate: true, persist: true)

@before_compile Contracts
@on_definition Contracts
end
end

defmacro __before_compile__(env) do
:ok = Agent.stop(__name__(env))
end
mod = env.module
predicates = Module.get_attribute(mod, :contract_predicates) |> Enum.reverse

def __name__(env), do: Module.concat(__MODULE__, env.module)
contract_funcs = predicates
|> Enum.map(&build_contract_function(&1, env))

defmacro requires(pre) do
Agent.update(__name__(__CALLER__), &%{&1 | pre: pre})
quote do
unquote_splicing(contract_funcs)
end
end

defmacro ensures(post) do
Agent.update(__name__(__CALLER__), &%{&1 | post: post})
def __on_definition__(env, kind, name, args, guards, body) when kind in [:def, :defp] do
mod = env.module
precond = Module.get_attribute(mod, :requires)
postcond = Module.get_attribute(mod, :ensures)

contract = %Contract{
func_name: name,
func_args: args,
func_guards: guards,
func_body: body,
}

if precond do
contract = %{ contract | precondition: precond}
Module.delete_attribute(mod, :requires)
end
if postcond do
contract = %{ contract | postcondition: postcond}
Module.delete_attribute(mod, :ensures)
end

if precond || postcond do
Module.put_attribute(mod, :contract_predicates, contract)
end

:ok
end
def __on_definition__(_env, _kind, _name, _args, _gaurds, _body), do: :ok

defp build_contract_function(%Contract{}=contract, env) do
mod = env.module
Module.make_overridable(mod, [{contract.func_name, contract.func_args |> length}])

body = quote do
unless unquote(contract.precondition), do: raise "Precondition not met: blame the client"
var!(result) = unquote(contract.func_body)
unless unquote(contract.postcondition), do: raise "Postcondition not met: blame yourself"

defmacro def(definition, do: content) do
%{pre: pre, post: post} = Agent.get(__name__(__CALLER__), &(&1))
var!(result)
end

ast = quote do
Kernel.def(unquote(definition)) do
unless unquote(pre), do: raise "Precondition not met: blame the client"
var!(result) = unquote(content)
unless unquote(post), do: raise "Postcondition not met: blame yourself"
if contract.func_guards |> length > 0 do
quote do
def unquote(contract.func_name)(unquote_splicing(contract.func_args)) when unquote_splicing(contract.func_guards) do
unquote(body)
end
end

var!(result)
else
quote do
def unquote(contract.func_name)(unquote_splicing(contract.func_args)) do
unquote(body)
end
end
end
Agent.update(__name__(__CALLER__), fn _ -> @default end)
end

ast
defmacro contract(predicate) do
Macro.escape(predicate)
end

# defmacro requires(predicate) do
# mod = __CALLER__.module
# Module.put_attribute(mod, :requires, predicate)
# end

# defmacro ensures(predicate) do
# mod = __CALLER__.module
# Module.put_attribute(mod, :ensures, predicate)
# end
end
8 changes: 4 additions & 4 deletions test/contracts_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ defmodule ContractsTest do

use Contracts

requires not full?(tank) && tank.in_valve == :open && tank.out_valve == :closed
ensures full?(result) && result.in_valve == :closed && result.out_valve == :closed
@requires contract not full?(tank) && tank.in_valve == :open && tank.out_valve == :closed
@ensures contract full?(result) && result.in_valve == :closed && result.out_valve == :closed
def fill(tank) do
%Tank{tank | level: 10, in_valve: :closed}
end

requires tank.in_valve == :closed && tank.out_valve == :open
ensures empty?(result) && result.in_valve == :closed && result.out_valve == :closed
@requires contract tank.in_valve == :closed && tank.out_valve == :open
@ensures contract empty?(result) && result.in_valve == :closed && result.out_valve == :closed
def empty(tank) do
%Tank{tank | level: 1, out_valve: :closed}
# %Tank{tank | level: 0, out_valve: :closed}
Expand Down