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
- Records and gradual types — the
types that
whererefinements attach to. - Pattern matching — the other place
match-style thinking shows up. - Errors and faults — catching and reading a refinement fault.