8.9 KiB
Contracts
Contracts are the portable runtime boundary-checking layer for tricu. A
contract is an ordinary tricu function that inspects a value and returns a
standard Result.
Contracts are not a type system. Tree Calculus is intensional: every value is a tree and can be inspected by any function. A contract can only observe a value and fail when it does not satisfy the advertised predicate. It cannot hide a value's representation or prove that an opaque function behaves correctly for all inputs.
Static typing for Tree Calculus is an area of active research. This document describes the dynamic-contract layer that exists today and the guarantees it can honestly claim.
1. The contract type
A contract is a function:
contract : Tree -> Tree -> Result Tree Tree
The second argument is the conventional rest slot. It takes a value and a rest
and returns one of the standard Result shapes from lib/base.tri:
ok value rest = pair true (pair value rest)
err msg rest = pair false (pair msg rest)
In contract contexts the rest slot is conventionally t. Two helpers make
this explicit:
contractOk = (value : (rest : ok value rest))
contractErr = (msg : (rest : err msg rest))
- On success, a contract returns the checked value. This may be the original value or a transformed/normalized value.
- On failure, it returns a reason. The reason is an arbitrary tree, often a string or a structured diagnostic.
Because a contract is just a tree-valued function, any Tree Calculus implementation can apply it. No special contract object format is required.
2. Core boundary wrappers
2.1 Explicit check
checkContract applies a contract with the conventional t rest slot and
returns the raw Result:
checkContract = (contract value : contract value t)
This is the most flexible form. The caller decides what to do with failure.
2.2 Direct boundary abort
withContract applies a contract with the conventional t rest slot and
continues on success, or calls a failure continuation on failure:
withContract = (contract value onOk onFail :
matchResult
(msg _ : onFail msg)
(checked _ : onOk checked)
(contract value t))
The failure continuation is supplied by the host or by the surrounding program. It may abort, log, return a default, or raise an effect. The core contract standard does not prescribe the failure behavior.
2.3 Example: a simple contract
isZero? = n :
equal? n 0
nat? = guardC "not a natural number" (n : gte? n 0)
-- explicit check
result = checkContract nat? 5
-- boundary abort
five = withContract nat? 5 (x : x) (msg : 0)
Real contract predicates are usually more interesting than isZero?; this
illustrates only the shape.
3. Contract combinators
Contracts compose using ordinary tricu functions. A few common patterns:
andC = (c1 c2 value rest :
matchResult
(msg _ : contractErr msg rest)
(v _ : c2 v rest)
(c1 value rest))
mapC = (f c value rest :
matchResult
(msg _ : contractErr msg rest)
(v _ : contractOk (f v) rest)
(c value rest))
listOf = (c xs rest : ...) -- checks spine and element contract
pairOf = (c1 c2 p rest : ...)
These are library code, not core standard. A contract library can provide
listOf, pairOf, fnContract, and similar helpers.
4. Higher-order contracts
A contract for a function value returns a wrapped proxy. The proxy itself is a contract: it checks arguments on the way in and results on the way out.
fnContract = (argC resC f rest :
contractOk
(x : (rest1 :
withContract argC x
(x' :
withContract resC (f x')
(y : contractOk y rest1)
(msg : contractErr msg rest1))
(msg : contractErr msg rest1)))
rest)
This does not prove that f is well-behaved internally; it only catches
violations at observed calls.
5. Interaction-tree contract effects
The core contract layer returns Result. For code that wants catchable,
composable contract failures without threading Result through every function,
contracts can be lifted into an interaction tree.
5.1 Interaction-tree constructors
These reuse the same pure/bind tags already used for tricu IO:
pureE value = pair 0 value
bindE action k = pair 1 (pair action k)
exceptE tag value k = pair 2 (pair tag (pair value k))
exceptE is resumable: k is the continuation. A handler may resume with
k replacement or abort by ignoring k. Contract failures usually abort; the
resumable shape is provided for generality and for richer effect handlers.
5.2 Lifting a contract
checkM contract value =
matchResult
(msg _ : exceptE "contract" msg (\_ : pureE t))
(checked _ : pureE checked)
(contract value t)
pureM and bindM are aliases for pureE and bindE:
pureM = pureE
bindM = bindE
5.3 Lifting pure functions
liftM f = (x : pureE (f x))
5.4 Example
halfM n =
bindM (checkM even? n)
(\n' : pureM (div n' 2))
use =
handleM "contract"
(\msg k : pureM 0)
(halfM 5)
handleM is a pure tree-to-tree function that interprets exceptE nodes,
either resuming with a replacement value or returning a failure tree.
5.5 Running a pure interaction tree
runM tree =
-- interprets pureE, bindE, and exceptE nodes
-- returns a Result or a residual effect tree
...
If the tree contains no IO or other host effects, runM can be written
entirely in tricu.
6. Source syntax
Source annotations are frontend sugar for inserting contract boundaries. They do not change the runtime semantics of ordinary code; they tell the frontend where to emit contract checks.
6.1 Argument and result assertions
idNat x@Nat =@Nat x
x@Nat inserts a Nat contract check on the argument. =@Nat inserts a check
on the result.
6.2 Compound contracts
sum xs@(List Nat) =@Nat ...
useHandler f@(Fn [(NonEmptyList String)] String) =@String ...
Compound annotations must be parenthesized when they contain application.
6.3 Phantom arguments
map @A @B =@(Fn [(Fn [A] B) (List A)] (List B)) ...
A phantom argument contributes a contract to the function boundary without introducing a term binder.
6.4 Missing annotations
Unannotated binders in a contract-bearing head default to Any. A missing
return annotation defaults to Any.
foo x y@Bool = body -- foo : Fn [Any Bool] Any, y : Bool
6.5 Export contracts
A module export may advertise a contract:
!export factorial : Fn [Nat] Nat
The advertised contract travels with the export in the module manifest.
7. Module and content-store integration
Contracts attach to module exports as ordinary content-addressed tree terms. There is no special contract object kind. The manifest references the contract with the same object kind as any other tree term:
name: "factorial"
object:
kind: arboricx.tree-term.v1
hash: <tree-term hash>
contract:
kind: arboricx.tree-term.v1
hash: <contract term hash>
The earlier arboricx.view-contract.type.v1 object kind is removed. A
contract is just a tree term.
For locally built modules, advertised export contracts may be checked before the manifest is published. For imported modules, the advertised contract is a boundary assumption. The local checker may insert guard wrappers when a contracted import is used.
See docs/module-system-design.md and
docs/content-store-and-module-format.md for the full store, manifest, and
bundle conventions.
8. Guarantees
The contract layer honestly claims only:
- A contract applied to a value returns a standard
Resultshape. withContractandcheckMinvoke the contract at the represented boundary.- A failed contract invokes the supplied failure continuation or
exceptEnode. - Content-addressed references prevent an attached contract from silently drifting to a different stored object.
- Provenance labels record where a contract assertion came from.
Only the contract function itself observes the runtime value. The rest is metadata plumbing.
9. Limitations
- Contracts do not establish parametricity or representation independence.
- They do not prove that opaque recursive or primitive code satisfies its contract for every input.
- They do not remove the need for tests, careful API design, or future static analysis.
- Higher-order contract wrapping has the usual costs and proxy-like behavior of dynamic contract systems.
10. Summary
- A contract is an ordinary
tricufunction:Tree -> Result Tree Tree. withContractaborts at a boundary;checkContractreturns the rawResult.- The interaction-tree layer (
checkM,bindM,handleM) adds catchable, composable failures on top of the same core contracts. - Contracts attach to module exports as ordinary tree-term objects.
- Provenance labels record source and blame, but do not prove truth.