5. Contracts

What this is for

A type says what shape a value has. Decimal<2> rules out a float and a string and a null. It cannot rule out negative, and almost everything that goes wrong with money is a number of the right shape and the wrong size.

So people write the rule down where they can — an assert, an if at the top of the body, a comment — and then this happens. Somebody needs the function to accept a case it refuses. Maybe a test, maybe an edge case at 5pm, maybe an agent that could not satisfy the check and took the shortest path to green. They delete one line.

Every test still passes. In fact more of them pass, because whatever was failing was failing on purpose. There is no compiler error, no warning, and nothing in the diff that looks different from any other deleted line — because in every other language an assertion in a body is just another line in a body.

That is the single most dangerous change anyone can make to a program, and this page is the reason Burxt can see it.

Think of a courier’s receipt

A courier hands you a parcel and a slip of paper with two signatures on it. Yours, saying what you handed over. Theirs, saying what came back.

Neither of you has to remember anything, and neither has to trust the other, because the slip says both halves. And if a signature is missing later, that is not an argument about what was agreed — it is a visibly incomplete piece of paper.

The slip, with both signatures withdraw signed on the way IN amount > $0.00 amount <= balance signed on the way OUT result >= $0.00 A signature that went missing withdraw signed on the way IN amount > $0.00 amount <= balance — gone signed on the way OUT result >= $0.00 Every test still passes. More of them than before. But the slip is visibly short a line — and burxt review reads slips.
Put the rule in the signature and it stops being a line in a body. It becomes a two-sided promise: what the caller must guarantee before it may call, and what the function guarantees in return. A deleted clause passes every test — more of them than before, because whatever was failing was failing on purpose — and is still visible, because burxt review reads declarations.

A step closer

function withdraw(balance: Decimal<2>, amount: Decimal<2>) -> Decimal<2>
    requires amount > $0.00
    requires amount <= balance
    ensures result >= $0.00
{
    return balance - amount;
}

Three claims no type can carry, in the one place everybody already reads. requires is checked on the way in; ensures on the way out.

the caller balance, amount requires the caller's side of it withdraw balance - amount ensures the function's side of it result >= $0.00 neither side has to trust the other's memory — both halves are in the signature

Nothing to remember, nothing to look up in another file, and nothing that depends on anyone having read the body.

In code

Named when they fail

burxt runtime error: `requires amount <= balance` failed in `withdraw`

The message quotes the clause exactly as you wrote it. A failure that says precondition violated sends you looking for which one; quoting it means the message is already the answer. Exit 70, like every other named runtime failure.

And now a tool can see the deletion

This is the payoff, and it only works because the promise is in the signature:

$ burxt review before.bx after.bx
WEAKENED  withdraw                           lost `requires amount <= balance`
WEAKENED  withdraw                           lost `ensures result >= $0.00`

2 weakened promise(s). A weakened contract is the one change that passes every test — the tests were failing BECAUSE of it.
$ echo $?
1

It exits non-zero, so in CI a promise cannot get quietly smaller. A tightened contract reports STRICTER and passes; a renamed parameter reports nothing at all, because clauses are compared structurally rather than as text.

Read that output next to the story at the top of this page. Same deletion, same green test suite — and one line of CI that says what happened.

There is no mode that removes them

No --release strips contracts. A flag that changed whether a program enforces its own stated invariants would mean its behaviour depends on how it was built, which is the class of thing this language refuses everywhere else. There is also no factory, wrapper or literal that gets around one — see the sealed box.

Checking costs real time in a hot loop. The answer is to put contracts on boundaries rather than on everything — not to make the checking optional.

result, and old(...)

result is bound inside ensures and nowhere else. It is not a keyword: a parameter may still be called result, it simply collides there, and the collision is an error rather than silent shadowing.

old(e) is the value on entry, evaluated once before the body runs. That is what makes a conservation law expressible — the thing an accountant would actually want checked:

class Ledger { from_side: Decimal<2>, to_side: Decimal<2> }

function (mutable self: Ledger) transfer(amount: Decimal<2>) -> Int
    requires amount > $0.00
    ensures self.from_side + self.to_side == old(self.from_side + self.to_side)
{
    self.from_side = self.from_side - amount;
    self.to_side = self.to_side + amount;
    return 0;
}

Money moved; none created, none destroyed. Lose a cent anywhere in that body and it stops, quoting the law:

burxt runtime error: `ensures self.from_side + self.to_side == old(self.from_side + self.to_side)` failed in `Ledger.transfer`

A shorter spelling: put the claim on the value

requires amount > $0.00 names amount in order to say which value it is about. Once a function has four parameters and three claims, the reader is matching names across six lines to work out what constrains what. So a claim can sit on the value it is about:

function withdraw(balance: Decimal<2> [> $0.00], amount: Decimal<2> [<= balance]) -> Decimal<2>
{
    return balance - amount;
}

That is exactly the same program as:

function withdraw(balance: Decimal<2>, amount: Decimal<2>) -> Decimal<2>
    requires balance > $0.00
    requires amount <= balance
{
    return balance - amount;
}

Same checks, same order, and the same failure message down to the byte — a test in the suite compiles both spellings and compares the two runs, because a claim like that is worth checking rather than asserting.

The subject is written into the message even though you did not write it:

burxt runtime error: `requires balance > $0.00` failed in `withdraw`

balance > $0.00, not > $0.00. A message that does not name the value which broke sends you back to the declaration to find out, and that is a cost paid on every failure forever.

A bracket on the return type is an ensures, and its subject is result:

function fee(amount: Decimal<2> [> $0.00]) -> Decimal<2> [>= $0.00] {
    return amount;
}

A bracket is a list of claims

The comma is and. || is or. Parentheses group.

function banded(v: Decimal<2> [it > $0.00, (it < $1000.00 || it > -$100.00)]) -> Decimal<2> {
    return v;
}

That is two claims, not three: the second is one claim with an || inside it. Break it and the message quotes it whole, parentheses included:

burxt runtime error: `requires (v < $1000.00 || v > -$100.00)` failed in `banded`

Which is the reason the comma exists at all, since you could always write one && instead: [a, b] tells you which one broke; [a && b] tells you only that something did. Same check, worse message — the same argument as the synthesized subject.

Two more things follow from the comma being and:

Clauses are checked left to right, and the first failure wins. [it > 0, it > 10, it > 100] given 5 reports n > 10 — not n > 0, which passed, and not n > 100, which was never reached.

A comma inside a call is not a separator. This is two clauses, not four:

pure function between(v: Int, lo: Int, hi: Int) -> Bool {
    return v > lo && v < hi;
}

function ranged(n: Int [between(it, 0, 100), it != 42]) -> Int {
    return n;
}

it, when the value appears twice

The leading form only reaches the left of a comparison. When the value is needed anywhere else, name it it:

function band(balance: Decimal<2> [it > $0.00 || it > -$100.00]) -> Decimal<2> {
    return balance;
}

function shout(word: String [len(it) > 0]) -> String {
    return word;
}

it is resolved in the message too — band reports balance > $0.00 || balance > -$100.00, for the same reason as above.

And it is not a keyword. Outside a bracket the name is free:

function count() -> Int {
    let it: Int = 7;
    return it;
}

A function with a parameter called it and a bracket that says it has two meanings for one word, so that is refused rather than silently shadowed — the same rule result follows inside ensures.

Spreading the elision across || was considered and rejected: [a > 0 || > 1] would be a rule you had to remember, and remembering rules is what this language tries not to charge you for.

pure — an answer that depends on its arguments and nothing else

pure function fee_for(amount: Decimal<2>) -> Decimal<2, RoundHalfEven> {
    return amount * 2.50%;
}

A pure function may not print, read or write a file, call into C, or call a function that is not itself pure. It is a claim, and the compiler holds you to it:

pure function fee_for(amount: Decimal<2>) -> Decimal<2, RoundHalfEven> {
    print("computing");
    return amount * 2.50%;
}
error: `pure function fee_for` may not print: a pure function's result must depend only on
       its arguments, which is the whole of what `pure` promises. Pass the value in as a
       parameter instead.

Contract clauses are checked under that same rule, and that is the real reason pure exists here. A clause that could print, mutate or call out would be a second program that only runs when somebody is looking — and a check that can change the answer is not a check.

pure and touches are the same claim from opposite ends, so saying both is a contradiction rather than a refinement, and the compiler says so.

decreases — this recursion ends

function countdown(n: Int, acc: Int) -> Int
    decreases n
{
    if n <= 0 { return acc; }
    return tail countdown(n - 1, acc + n);
}

The measure is evaluated with the new arguments at each recursive call and compared against the current one: strictly smaller, and never negative.

burxt runtime error: `decreases n` did not decrease on a recursive call to `walk`

Checking at the call site rather than on entry is what makes this work with return tail, where the frame that would have remembered the old value is already gone.

The measure must be an Int. A Decimal measure invites a descent that shrinks forever without arriving — 1.00, 0.50, 0.25, 0.125 — which is the exact failure the clause exists to rule out.

Why it is built this way

A claim in a signature is a claim a tool can read. That is the entire reason contracts are not assert in a body. assert amount <= balance is a line among lines: delete it and the diff shows one plausible removal. requires amount <= balance is part of the declaration, so deleting it changes what the function promises, and burxt review exits non-zero.

The message is the answer. A failure quotes the clause exactly as you wrote it, rather than saying precondition violated and leaving you to find which one.

And the same clause does a second job. burxt mcp-schema derives an MCP tool’s JSON Schema from these preconditions — so the bound an agent is validated against and the bound the function enforces are one sentence. That is only possible because the claim is in the signature.

There is no mode that removes them. No --release strips contracts. A flag that changed whether a program enforces its own stated invariants would mean its behaviour depends on how it was built, which is the class of thing this language refuses everywhere else.

What it costs

Checking costs real time in a hot loop. The answer is to put contracts on boundaries rather than on everything — not to make the checking optional.

They are runtime checks, not proofs.

Static proof of arbitrary contracts is SMT-solver territory, and a prover that is right sometimes is worse than a check that is right always. Static proof is the eventual goal; this is what is reachable and true today. (The design record, including what a static pass would need.)

An ensures cannot bind result to a class yet. A class travels back through a hidden pointer into the caller’s storage, and binding result to that needs care a scalar does not. Return a scalar, or drop the clause.

A clause relating two parameters has no JSON Schema key. It is still enforced; burxt mcp-schema reports that it could not carry it rather than approximating. See tools and agents.

When you reach for it

You want to say Write
this argument must be positive Int [> 0], or requires n > 0
this amount must not exceed that one requires amount <= balance — a bracket cannot relate two parameters
the answer is never negative -> Decimal<2> [>= $0.00], or ensures result >= $0.00
this call changed the balance by exactly the amount ensures self.balance == old(self.balance) - amount
this function reads nothing and touches nothing pure function
this recursion ends decreases n
a bound an agent calling this tool must respect put it on the value: mcp-schema reads it

Put them on boundaries — the edge of a module, a constructor, anything an agent will call. A contract on every private helper costs time and tells a reviewer nothing new.

Examples

A contract firing, and naming itself. The bracket form and the requires form are the same sentence, so this uses the short one:

function withdraw(balance: Decimal<2>, amount: Decimal<2> [> $0.00, <= balance])
    -> Decimal<2> [>= $0.00]
{
    return balance - amount;
}

print(withdraw($100.00, $30.00));
print(withdraw($100.00, $200.00));
70.00
burxt runtime error: `requires amount <= balance` failed in `withdraw`

The first call answers. The second stops the program, and the message quotes the clause you wrote rather than saying which of three it might have been.

pure and decreases, together. pure says the answer depends on the arguments and nothing else; decreases n says this recursion ends:

pure function factorial(n: Int [>= 0, <= 20]) -> Int
    decreases n
{
    if n <= 1 { return 1; }
    return n * factorial(n - 1);
}

print(factorial(5));
print(factorial(10));
120
3628800

The <= 20 is not decoration: 21! does not fit in an Int, and an overflow would stop the program. The clause turns that into a refusal at the call instead.

Next

Effects — what a function is allowed to reach, and why that belongs in the signature too.