Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.