Files
tricu/docs/contracts.md

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:

  1. A contract applied to a value returns a standard Result shape.
  2. withContract and checkM invoke the contract at the represented boundary.
  3. A failed contract invokes the supplied failure continuation or exceptE node.
  4. Content-addressed references prevent an attached contract from silently drifting to a different stored object.
  5. 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 tricu function: Tree -> Result Tree Tree.
  • withContract aborts at a boundary; checkContract returns the raw Result.
  • 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.