From ac1ca503273a9385a0377f5f519741672caf2a99 Mon Sep 17 00:00:00 2001 From: X4lldux Date: Wed, 16 Mar 2016 12:38:41 +0100 Subject: [PATCH 1/2] do not redefine `def` macro and use attribute tags Changes the implementation to not redefine `def` macro. Additionaly, pre- and post-conditions are now attributes, since specifying a contract for a function, feels more as a "tagging" it with a contract. --- lib/contracts.ex | 99 ++++++++++++++++++++++++++++++++--------- test/contracts_test.exs | 8 ++-- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/lib/contracts.ex b/lib/contracts.ex index 4ccd479..0ecef07 100644 --- a/lib/contracts.ex +++ b/lib/contracts.ex @@ -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 diff --git a/test/contracts_test.exs b/test/contracts_test.exs index 8a5f695..4424705 100644 --- a/test/contracts_test.exs +++ b/test/contracts_test.exs @@ -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} From 738a22d24a8ea4751c773d761797d7e6dcafacfb Mon Sep 17 00:00:00 2001 From: X4lldux Date: Wed, 16 Mar 2016 13:05:25 +0100 Subject: [PATCH 2/2] fix missing '__' in `__on_definition__` function --- lib/contracts.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/contracts.ex b/lib/contracts.ex index 0ecef07..9749b50 100644 --- a/lib/contracts.ex +++ b/lib/contracts.ex @@ -26,7 +26,7 @@ defmodule Contracts do end end - def on_definition(env, kind, name, args, guards, body) when kind in [:def, :defp] do + 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)