SCUA is a dynamic scripting language, so most of it will feel familiar. Four ideas set it apart from the usual scripting language, and they're worth getting straight before the rest of the docs. This page explains each one and points you at the guide page that covers it in full.
#Dynamic by default, gradually typed
You can write SCUA with no type annotations at all. Variables hold whatever you put in them, functions take whatever you pass, and nothing is checked until it runs. That's the default, and it's a complete way to use the language.
fn describe(x)
return `got: {x}`
end
print(describe(42))
print(describe("a string"))
print(describe([1, 2, 3]))
$ scua describe.scua
got: 42
got: a string
got: [1, 2, 3]
When you want a guarantee, add annotations. The compiler checks them before the program runs and reports mistakes with a line number.
fn area(w: number, h: number) -> number
return w * h
end
print(`area = {area(3, 4)}`)
$ scua area.scua
area = 12
Pass an argument of the wrong type and you hear about it at compile time, not in production:
fn area(w: number, h: number) -> number
return w * h
end
print(area(3, "four"))
$ scua area.scua
scua: area.scua:4: type error: argument 2 to 'area' is string, expected number
One thing matters here: annotations are erased. They drive the compile-time check and then they're gone. A program runs the same with them or without them, so adding a type can catch a bug but can never change behavior. Annotate the parts of a program you want to nail down, leave the rest dynamic, and mix the two freely. Records and the full type syntax are covered in Records and gradual types.
#Partitions, and why state is cheap to persist
State in SCUA lives in partitions. A partition is an isolated region of memory with its own data. What makes it useful is the memory layout: a partition's bytes are position-independent. Nothing inside points at a fixed memory address, so the whole partition is just a blob of bytes that means the same thing wherever it lives.
That property is what makes persistence cheap. To save a partition you write its bytes out. To restore it, on the same machine or a different one, you read those bytes back. You don't write serialization code, hand-maintain a schema, or turn objects into JSON and back. The state that a partition holds, a per-player inventory, the state of one match, an NPC's memory, can be parked on disk and resumed later as-is.
You don't need to know how the memory manager achieves this to use it. What you do need to know is the shape it gives your program: put state that should persist or move together into the same partition. Saving, loading, and migrating that state is walked through in Save, load, and migrate game state.
Saving and loading happens from the host: SCUA runs embedded in another program (a game engine, a server, a tool), and that host serializes a partition to bytes and reads them back, with no serialization code on either side. If you're writing the host rather than the scripts, Embed SCUA in a host program shows the C interface end to end.
#The actor model
Partitions are isolated from each other. One partition cannot reach into another's memory, so there is no shared mutable state and no data races to reason about. They coordinate by passing messages.
A partition declares state fields and handlers. You send
it a message two ways: tell is fire-and-forget, and
ask waits for a reply. Only the partition's own handlers
run against its state, one message at a time, so a handler never has to
lock anything or worry about another thread mid-update.
partition Counter
state n = 0
on Bump(by)
n = n + by
print(`counter is now {n}`)
end
end
let c = Counter()
tell c.Bump(3)
tell c.Bump(4)
$ scua counter.scua
counter is now 3
counter is now 7
Counter() creates an instance. on Bump(by)
is a handler. Inside it, n is the partition's own state,
and each tell delivers one message that the handler
processes start to finish. Messages are copied as they're sent, which is
the same isolation that makes a partition safe to persist. The full
model, including ask and replies, request timeouts, and
passing references between partitions, is in Partitions and the actor
model.
#Two kinds of failure
SCUA splits failure into two categories, and keeps them apart on purpose.
The first is expected failure: a lookup that misses, input
that doesn't parse, a charge that exceeds a balance. These are values. A
function that might fail returns Ok(value) or
Error(message), the caller decides what to do, and the
postfix ? operator unwraps an Ok or returns
the Error to its own caller.
fn charge(balance, amount)
if amount > balance then return Error("insufficient funds") end
return Ok(balance - amount)
end
match charge(100, 30)
Ok(left) -> print(`charged, {left} left`)
Error(e) -> print(`declined: {e}`)
end
$ scua charge.scua
charged, 70 left
The second is a fault: a bug, like dividing by zero or
hitting a case you thought was impossible. Faults aren't ordinary values
you pass around. They unwind until something catches them with
try ... rescue, which is your chance to log and recover
instead of crashing.
fn safe_div(a, b)
try
return Ok(a // b)
rescue err
return Error(`math error: {err}`)
end
end
match safe_div(1, 0)
Ok(x) -> print(x)
Error(e) -> print(e)
end
$ scua safe_div.scua
math error: divide by zero
The rule of thumb: use a Result when a caller can
reasonably be expected to handle the failure, and let faults represent
the cases that mean your code is wrong. Both are covered in Errors and faults.