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)
"hello\n"            # string (str)
true                 # boolean
false                # boolean

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

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.

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 -> { ... }
}

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.