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
const 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.

matches has one other meaning: r matches Ok and r matches Error test a Result. Those two names are reserved, so there is no contract to confuse them with; naming a contract Ok or Error is a compile error.

A clause, like a where refinement, is a condition, so a clause that produces a Result or a flags set is an error rather than always true: write check(x) matches Ok, or Flag.A in x.mask, in the clause instead. x matches C is always true or false, and every clause is tested, the last one included.

#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
  binding 'bad': bad.value violates Stat's where rule (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.

Either kind of where can say why, with else "reason". The reason leads the fault's message, and scua schema publishes it as the field's description, so whoever fills the record in learns the rule:

record Tool { title: string where title.len() >= 1 else "a title is required" }
try
  let t: Tool = { title = "" }
rescue e
  print(e.message)
end
$ scua tool.scua
binding 't': t.title: a title is required (field 'title' of Tool, got )

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