Bank example¶
The bank example shows the relation algebra's numeric aggregation side. The
trusted module exposes a single side-effecting withdraw(account, amount)
function, and the contract vocabulary covers two domain predicates:
max_spend(limit) (a sum over all withdrawals) and only_account(account)
(a universal over the account field).
Trusted module¶
The trusted effect is a stub with a non-negativity precondition. The prover lifts that precondition into a proof obligation at every call site.
examples/bank/tools/bank/trusted/effects.py
import deal
@deal.pre(lambda account, amount: amount >= 0, message="amount must be non-negative")
@deal.has("trusted")
def withdraw(account: str, amount: int) -> None:
"""MOCK trusted withdrawal.
Contract: amount must be non-negative.
"""
pass
@deal.post(lambda result: result >= 0, message="balance must be non-negative")
@deal.has("bank_read")
def balance(account: str) -> int:
"""MOCK trusted balance lookup (amount outstanding on an account).
Contract: the returned balance is non-negative.
"""
return 0
Two contract helpers wrap the generic effect("withdraw") relation.
examples/bank/tools/bank/trusted/contracts.py
"""Bank predicates built from generic trusted effect facts."""
from clauz3.spec import ContractSpec, contract, effect
Withdraw = effect("withdraw")
@contract
def max_spend(limit: int) -> ContractSpec:
"""Guarantee that total withdrawals are at most limit."""
return Withdraw.sum(lambda w: w.amount) <= limit
@contract
def only_account(account: str) -> ContractSpec:
"""Guarantee that every withdrawal targets this account."""
return Withdraw.all(lambda w: w.account == account)
A passing case¶
The sum of two withdrawals is exactly the spending limit, so
bank.max_spend(5) is discharged.
examples/bank/cases/max_spend_pass.py
# ruff: noqa: F821
from tools.bank.trusted import contracts as bank
from tools.bank.trusted.effects import withdraw
import clauz3
@clauz3.guarantee(bank.max_spend(5))
def main() -> None:
withdraw("checking", 2)
withdraw("checking", 3)
A failing case¶
Bumping one of the amounts above the limit causes the proof to fail.
examples/bank/cases/max_spend_fail.py
# ruff: noqa: F821
from tools.bank.trusted import contracts as bank
from tools.bank.trusted.effects import withdraw
import clauz3
@clauz3.guarantee(bank.max_spend(5))
def main() -> None:
withdraw("checking", 2)
withdraw("checking", 4)
All cases¶
Browse the full set on the all-cases page.
How they run¶
examples/bank/Justfile
trusted := "tools/bank/trusted"
effects := trusted + "/effects.py"
prove := "uv run clauz3 prove --trusted-root " + trusted
test: lint cases
lint:
uv run python -m deal lint {{effects}}
cases: max-spend-pass max-spend-fail max-spend-branch-pass for-loop-sum-pass for-loop-sum-fail only-account-pass only-account-fail negative-amount-fail pay-bill-capped-pass pay-bill-uncapped-fail pay-bill-cap-too-tight-fail
max-spend-pass:
{{prove}} cases/max_spend_pass.py
max-spend-fail:
if {{prove}} cases/max_spend_fail.py; then exit 1; fi
max-spend-branch-pass:
{{prove}} cases/max_spend_branch_pass.py
for-loop-sum-pass:
{{prove}} cases/for_loop_sum_pass.py
for-loop-sum-fail:
if {{prove}} cases/for_loop_sum_fail.py; then exit 1; fi
only-account-pass:
{{prove}} cases/only_account_pass.py
only-account-fail:
if {{prove}} cases/only_account_fail.py; then exit 1; fi
negative-amount-fail:
if {{prove}} cases/negative_amount_fail.py; then exit 1; fi
pay-bill-capped-pass:
{{prove}} cases/pay_bill_capped_pass.py
pay-bill-uncapped-fail:
if {{prove}} cases/pay_bill_uncapped_fail.py; then exit 1; fi
pay-bill-cap-too-tight-fail:
if {{prove}} cases/pay_bill_cap_too_tight_fail.py; then exit 1; fi