State transitions
There are three primary control-flow operations in Lake: self(args) for in-place state changes, machine(args) for spawning a new process, and calling a pid to send a message. All three look like calls; what they do depends on the callee.
§self(args) — internal transition
self(args) re-enters the current machine's branch dispatch with new arguments. It does not allocate, does not return, and does not push a stack frame.
@rt(rt_write)
counter is {
0 i64 -> { rt_write(1 "done\n" 5) }
n i64 -> { self(n-1) }
}
main is {
_ -> { counter(5) }
}self(n-1) does not recurse on the call stack — it transitions the process to a new state and the scheduler re-enters the machine. Each self consumes one reduction.
Argument expressions are arbitrary:
sum is {
0 i64 acc i64 -> { rt_write(1 "done\n" 5) }
n i64 acc i64 -> { self(n-1 acc+n) }
}§machine(args) — spawn
Calling any machine other than self spawns a new concurrent process.
@rt(rt_write)
worker is {
0 i64 -> { rt_write(1 ".\n" 2) }
n i64 -> { self(n-1) }
}
main is {
_ -> {
worker(500)
worker(500)
worker(500)
}
}Each worker(...) allocates a process, registers it with the scheduler, and returns a pid immediately. The spawning process never blocks on the spawnee — they continue in parallel.
The pid returned by a spawn can be discarded (as above) or stored:
let p pid = worker(500)§Cooperative scheduling
All spawned processes share one OS thread (in this MVP). The scheduler is round-robin with a fixed quantum of 256 reductions per process. A reduction is roughly one CPS block — a self, a wait, a runtime call, the entry into a branch.
This means concurrent processes make interleaved progress without preemption. There are no priorities and no fairness guarantees beyond round-robin.
§Sending a message — pid(args)
If the callee is a pid, the call is a send, not a spawn.
let p pid = receiver()
p(42) // queue 42 in receiver's mailboxMessages are placed in the receiver's mailbox (a 256-slot ring buffer). If the receiver is currently in a wait { ... }, sending wakes it up and moves it back into the active queue.
The send itself returns immediately and is non-blocking. If the mailbox is full, the message is dropped. (This will become configurable.)
§Runtime calls — inlined
Calls to names declared with @rt(...) are not state transitions — they are inlined and run synchronously in the current process.
rt_write(1 "hello\n" 6) // direct call, no schedulingSee Directives for the available runtime functions.