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

Expressions

Expressions make up the body of branches.

Literals

42                   # number (i64)
0                    # number (i64)
100000               # number (i64)
0x7fffffff           # hexadecimal number (i64)
"hello\n"            # string literal (str)
b"GET "              # byte-string literal (buf)
'A'                  # char literal (i64 — the byte value, 65)
true                 # boolean
false                # boolean

Numbers are i64 and may be written in decimal or hex (0x...). String literals (str) support escape sequences: \n, \t, \r, \\, \".

A byte-string literal b"..." has type buf instead of str — useful where an API wants a runtime byte buffer rather than a static string span.

A char literal 'X' is sugar for the byte’s integer value (there is no dedicated char type): 'A' is 65, '\n' is 10, '\xff' is 255. It reads more clearly than a bare number when working with byte data:

when at(b i) == '\n' {
  true -> { ... }
  _    -> { ... }
}

Variables

Names declared in branch patterns:

counter is {
  n i64 -> {
    self(n-1)        # "n" is available here
  }
}

Let Bindings

let x i64 = 42

Creates a local variable within the current branch.

Arithmetic

Binary operators with precedence:

PrecedenceOperatorsDescription
10 (highest)* /Multiplication, division
9+ -Addition, subtraction

All operators are left-associative and operate on i64:

n - 1                # subtraction
acc + n              # addition
acc1 + acc2          # addition
steps - 1            # subtraction

Arithmetic works in arguments:

self(n-1 acc+n)                # two computed arguments
self(steps-1 acc2 acc1+acc2)   # three, one with addition

Comparisons

PrecedenceOperatorsDescription
8<= >= == < >Comparison
0 == n               # equality check
1 <= steps           # less-or-equal check

Lower precedence than arithmetic: a + b <= c means (a + b) <= c.

Lake has no != operator. Test for inequality by matching == in a when and handling the non-matching case in a _ (or false) arm.

Bitwise and Shift Operators

Integers can also be combined bit by bit:

OperatorsDescription
<< >>left / right shift (logical)
&bitwise AND
^bitwise XOR
|bitwise OR

These treat i64 as a raw bit pattern. They are common in byte-level code and as a stand-in for the (absent) modulo operator when the divisor is a power of two — x & (n - 1) computes x % n:

let slot = hash & (cap - 1)
let hi   = word >> 8 & 0xff

Parenthesize freely to make precedence explicit, e.g. (a & (b + 1)).

Indexing

A buf supports byte indexing with buf[i], which reads the byte at offset i as an i64:

let first = b[0]

When Expressions

Conditional branching based on an expression:

when 0 == n {
  true  -> { rt_write(1 "done\n" 5) }
  false -> { self(n-1) }
}
when 1 <= steps {
  true  -> { self(steps-1 acc2 acc1+acc2) }
  false -> { rt_write(1 ".\n" 2) }
}

Syntax:

when condition {
  pattern -> { body }
  pattern -> { body }
}

The condition is any expression. Arms match on literal values (numbers, booleans). If no arm matches, execution falls through silently.

Numeric pattern matching is also supported:

when some_value {
  0 -> { ... }
  1 -> { ... }
  2 -> { ... }
}

when also matches enum variants, binding each variant’s payload. Matching over an enum must be exhaustive (cover every variant or include a _ arm):

when r {
  Ok(n)  -> { ret n }
  Err(_) -> { ret 0 - 1 }
}

Wait Expression

The wait expression suspends the current process until a message arrives in its mailbox:

wait {
  n i64 -> { rt_write(1 "received\n" 9) }
}

When a message arrives, it is dequeued from the mailbox and the handler body executes with the message value bound to the pattern variables.

If the mailbox is empty, the process is suspended and moved to the scheduler’s wait array. When another process sends a message, the waiting process is awakened.

Multiple messages can be handled by using wait in a loop:

receiver is {
  remaining i64 -> {
    when 1 <= remaining {
      true -> {
        wait {
          n i64 -> {
            rt_write(1 "." 1)
            self(remaining-1)
          }
        }
      }
    }
  }
}

main is {
  _ i64.0 -> {
    let r pid = receiver(3)
    r(1)
    r(2)
    r(3)
  }
}

Message Sending

Calling a pid-typed variable sends a message to that process:

let p pid = worker()
p(42)                    # send 42 to the worker process

The syntax is identical to calling a machine, but when the callee is a pid variable, it becomes a message send instead of a spawn.

Messages are enqueued in the target process’s mailbox (a ring buffer of 256 slots). If the target is waiting, it is immediately awakened and moved back to the scheduler’s active queue.

Ping-Pong Example

ponger is {
  _ i64.0 -> {
    wait { partner pid -> { self(partner 3) } }
  }
  partner pid remaining i64 -> {
    when 1 <= remaining {
      true -> {
        wait { n i64 -> { partner(1) self(partner remaining-1) } }
      }
    }
  }
}

pinger is {
  partner pid remaining i64 -> {
    when 1 <= remaining {
      true -> {
        partner(1)
        wait { n i64 -> { self(partner remaining-1) } }
      }
    }
  }
}

main is {
  _ i64.0 -> {
    let po pid = ponger()
    let pi pid = pinger(po 3)
    po(pi)
  }
}

The ponger waits to receive the pinger’s pid, then they exchange messages in a loop.