State Transitions
State transitions are the primary control flow mechanism in Lake. There are two kinds:
self() — Internal State Transition
self(args) transitions the current process to a new state without spawning a new process. The current branch’s variables are replaced with the new arguments, and execution restarts from the matched branch.
This is the primary looping mechanism:
counter is {
n i64 -> {
when 0 == n {
true -> { rt_write(1 "done\n" 5) }
false -> { self(n-1) }
}
}
}
self(n-1) does not recurse on the call stack — it transitions the process state and the scheduler re-enters the machine.
Arguments can include arithmetic:
sum is {
n i64 acc i64 -> {
when 0 == n {
true -> { rt_write(1 "done\n" 5) }
false -> { self(n-1 acc+n) }
}
}
}
machine(args) — Spawn a New Process
Calling any machine other than self spawns a new concurrent process:
main is {
_ i64.0 -> {
counter(5)
counter(3)
counter(7)
}
}
Each counter(N) spawns an independent process. The spawning process continues immediately — it does not wait for the spawned process to finish.
Cooperative Scheduling
All spawned processes are managed by a cooperative scheduler. Each process runs a quantum of work (256 blocks) before yielding. This means concurrent processes make interleaved progress:
@rt(rt_write)
worker is {
steps i64 acc1 i64 acc2 i64 -> {
when 1 <= steps {
true -> { self(steps-1 acc2 acc1+acc2) }
false -> { rt_write(1 ".\n" 2) }
}
}
}
main is {
_ i64.0 -> {
worker(100000 0 1)
worker(100000 0 1)
worker(100000 0 1)
worker(100000 0 1)
worker(100000 0 1)
worker(100000 0 1)
worker(100000 0 1)
worker(100000 0 1)
}
}
Eight worker processes execute concurrently, each computing 100,000 Fibonacci iterations.
pid(args) — Send a Message
Calling a pid-typed variable sends a message to that process instead of spawning:
receiver is {
_ i64.0 -> {
wait { n i64 -> { rt_write(1 "got it\n" 7) } }
}
}
main is {
_ i64.0 -> {
let p pid = receiver()
p(42) # send message, not spawn
}
}
When you call a machine, it returns a pid. You can store that pid and send messages to it later.
Messages are enqueued in a 256-slot ring buffer. If the target process is suspended (via wait), it is immediately awakened.
See Expressions for details on wait and message sending.
Runtime Functions
Calls to @rt-declared functions are inlined — they execute immediately without spawning a process:
rt_write(1 "hello\n" 6) # direct call, no process spawned
See Directives for the available runtime functions.