SCUA

Manual

Contracts

A contract is a named set of conditions over some values. It's a predicate you can name, reuse, and compose. Call it and you get back a report of which conditions held and which didn't. It evaluates every clause, not just the first one that fails. That makes contracts a good fit for gating logic ("can this entity sprint?") and for player-facing checklists ("1 of 3 objectives done").

contract Adult(p) p.age >= 18 end
print(Adult({ age = 25 }).ok)
print(Adult({ age = 12 }).ok)

Run it:

$ scua adult.scua
true
false

A single-clause contract is just a named predicate. Call it and read .ok for the boolean.

#Clauses and the report

Most contracts have several clauses, each with a label and an optional else reason explaining what to fix:

contract LevelComplete(player, world)
  gold: player.gold >= 100              else "collect 100 gold"
  boss: world.boss_dead                 else "defeat the boss"
  time: player.time <= world.par_time   else "finish under par time"
end

Calling it returns a report. The report has ok (did everything pass), done and total (how many of how many), and clauses, a list with one entry per clause. Each clause entry carries its name (the label), ok, a why (the else reason), and, when the clause is a comparison, the actual and target values. That's enough to drive a "1 of 3 objectives" UI directly:

contract LevelComplete(player, world)
  gold: player.gold >= 100              else "collect 100 gold"
  boss: world.boss_dead                 else "defeat the boss"
  time: player.time <= world.par_time   else "finish under par time"
end

fn show(player, world)
  let r = LevelComplete(player, world)
  print(`Level complete? {r.ok}  ({r.done} of {r.total})`)
  for c in r.clauses do
    if c.ok then
      print(`  [x] {c.why}`)
    else
      if c.target == nil then
        print(`  [ ] {c.why}`)
      else
        print(`  [ ] {c.why}  ({c.actual}/{c.target})`)
      end
    end
  end
end

show({ gold = 120, time = 83 }, { boss_dead = false, par_time = 60 })
$ scua objectives.scua
Level complete? false  (1 of 3)
  [x] collect 100 gold
  [ ] defeat the boss
  [ ] finish under par time  (83/60)

A boolean clause like boss has no numeric progress, so its actual and target are nil. A comparison clause like time fills them in, here 83 against a target of 60.

Contracts are meant to be pure: anything dynamic (the clock, a feature flag) is passed in as a parameter rather than read from the outside. That keeps a contract replayable.

#Composing contracts

A clause can call another contract and read its result. Build small predicates and combine them:

contract Adult(p) p.age >= 18 end
contract CanEnterTournament(p)
  Adult(p).ok    else "must be 18+"
  p.rank >= 10   else "reach rank 10"
end
let r = CanEnterTournament({ age = 25, rank = 4 })
print(`eligible? {r.ok}`)
for c in r.clauses do
  if not c.ok then print(`  missing: {c.why}`) end
end
$ scua compose.scua
eligible? false
  missing: reach rank 10

#The boolean shortcut: matches

The full report allocates a list of clauses. On a hot path, where you only need a yes or no, that's wasted work. value matches Contract inlines the clauses to a plain boolean with no allocation:

contract CanSprint(p)
  p.stamina >= 20
  p.grounded
end
let player = { stamina = 35, grounded = true }
if player matches CanSprint then
  print("sprinting")
end
print({ stamina = 5, grounded = true } matches CanSprint)
$ scua sprint.scua
sprinting
false

The split is deliberate: call the contract directly when you want the report to show a player what's left, use matches when you just need to gate something per frame or per entity.

#The in operator

in tests membership and reads naturally inside a clause or anywhere else. x in [a, b, c] checks an array. x in lo:hi checks a half-open range, the same [lo, hi) span as a for loop or a slice, so 1:101 covers 1 through 100:

contract ValidHero(h)
  cls:   h.cls in ["mage", "rogue", "warrior"]  else "pick a known class"
  level: h.level in 1:101                       else "level must be 1..100"
end
print(ValidHero({ cls = "mage", level = 42 }).ok)
print(ValidHero({ cls = "bard", level = 42 }).ok)
$ scua valid-hero.scua
true
false

Because the range is half-open, 100 in 1:100 is false; write 1:101 if you mean to include 100.

#Refinements with where

A contract can be attached to a record or one of its fields as a where refinement. It's a pure predicate, with the record's field names in scope, checked whenever data crosses into that type: a typed binding, a parameter whose type is the record, a persistence load. Inside typed code it's erased.

You attach the refinement to a named record or type, not to a parameter directly. There is no inline fn f(n: int where Positive) form (it's a syntax error). Give the parameter a type that carries the where, and the check runs when an argument crosses in.

contract NonNeg(n) n >= 0 end
record Stat { value: int where NonNeg, cap: int } where value <= cap

let s: Stat = { value = 30, cap = 100 }
print(`stat {s.value}/{s.cap}`)

let loose = { value = -5, cap = 100 }
try
  let bad: Stat = loose
rescue e
  print(`rejected {e.record}.{e.field} = {e.value}`)
  print(`  {e.message}`)
end
$ scua stat.scua
stat 30/100
rejected Stat.value = -5
  field 'value' of Stat violates its `where` constraint (got -5)

Here a bare contract name on a field (value: int where NonNeg) is the non-allocating check, the same as value matches NonNeg. A where clause after the record body (where value <= cap) constrains the record as a whole.

Be clear about the guarantee. The refinement is checked at the boundary and then trusted. It is not re-checked on every later write, so a mutation afterward can push the value back out of range without anyone noticing. The promise is "valid when it crossed in," not "valid at every point in the program."

#Introspecting a boundary fault

When loose data violates a refinement at a boundary, you get a fault you can catch and inspect. As the example above shows, the fault carries e.record, e.field, and e.value for precise triage, plus e.message for a ready-made summary. See errors and faults for how try/rescue works.

#See also