Lake

Expressions

Expressions make up the body of branches.

§Literals

lake
42                    // i64
0                     // i64
"hello\n"             // str
true                  // bool
false                 // bool

Numbers are i64. Strings support escapes: \n, \t, \r, \\, \".

§Variables

Names introduced by typed parameters in the enclosing branch:

lake
counter is {
  n i64 -> {
    self(n-1)         // n is in scope
  }
}

§Let bindings

lake
let x i64 = 42

Creates a local variable in the current branch's scope.

§Arithmetic

PrecedenceOperators
10* /
9+ -

All operators are left-associative and operate on i64.

lake
n - 1
acc + n
acc1 + acc2

Arithmetic is allowed in argument positions:

lake
self(n-1 acc+n)

§Comparisons

PrecedenceOperators
8<= >= == < >
lake
0 == n
1 <= steps

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

§When expression

when is an inline match on a value.

@rt(rt_write)

main is {
  _ -> {
    when 1 + 1 {
      0 -> { rt_write(1 "zero\n" 5) }
      1 -> { rt_write(1 "one\n"  4) }
      2 -> { rt_write(1 "two\n"  4) }
    }
  }
}

Arms match on literal values (numbers, booleans, or strings). If no arm matches, the when falls through silently.

The same machine could also be expressed without when by using literal-guard branches — see Branches & patterns. Use when for ad-hoc matching inside a body, and literal-guard branches for whole-machine dispatch.

§Wait expression

wait suspends the current process until a message arrives in its mailbox, then handles it.

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

When a message arrives, it is dequeued from the mailbox and the matching handler runs with the message bound to its pattern.

If the mailbox is empty, the process is moved to the scheduler's wait array. As soon as another process sends, it is woken up.

Multiple messages? Wrap wait in a self-loop.

@rt(rt_write)

receiver is {
  0 i64 -> {}
  remaining i64 -> {
    wait {
      n i64 -> {
        rt_write(1 ".\n" 2)
        self(remaining-1)
      }
    }
  }
}

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

§Sending a message

Calling a pid value sends — it does not spawn.

lake
let p pid = worker()
p(42)                    // send 42 to the worker

Messages are queued in a 256-slot ring buffer. If the receiver is currently waiting, sending wakes it up.

§Receive once

@rt(rt_write)

receiver is {
  _ -> {
    wait {
      n i64 -> { rt_write(1 "got it\n" 7) }
    }
  }
}

main is {
  _ -> {
    let r pid = receiver()
    r(42)
  }
}

main spawns receiver and stores the resulting pid in r. The receiver is suspended on wait until r(42) enqueues a message; the handler then prints got it and the process finishes.

Building richer protocols (ping-pong, supervisors, streaming) requires juggling pids through a small handshake. See the project's examples/ directory.