Processes, Messages, Case

I think I’ve teased it long enough - let’s get into what (I think) sets Xlerb apart from other stack languages. Concurrent processes doing stacky things, with preemption and fairness guarantees.

Let’s take some first steps, and send a message.



Wait, send it where? To whom?

We need someone to send our message to! Spawning someone else (a process) is as simple as pushing a quotation, and spawning it:

xlerb[0]> ["hello from " write self inspect write "!" write] spawn
hello from #PID<0.134.0>!
xlerb[1]> 

We get a spawned process, which executed the quotation, which printed its own PID out to the console. As the caller, we also get it back on the stack:

xlerb[1]> .s
#PID<0.134.0>

Sending a message is as easy as pushing your message (a stack, of course), and calling send:

xlerb[1]> ! self :ok "hello" 100 send
xlerb[1]> 

! pushes a “message-start” token onto the stack. You then push your message’s elements, then send. send works by winding back the stack until it hits the message-start token, then packs it all up and sends it to the PID it expects immediately before the message-start.

Now, how do we receive something? The simplest way is with whatever:

xlerb[1]> self ! :hello send whatever receive
:hello

This in a little loop will make a nice echo-server for us:

xlerb[0]> [whatever receive .s flush recurse] spawn
xlerb[1]> ! :ping send
  > :ping
xlerb[1]> ! self :ping_from send
  > :ping_from
    #PID<0.1.0>

We can see it receives anything, then prints out, and flushes its stack. recurse is a special word for quotations, which invokes a tail-call. This lets us have recursive quotations without having to name them.

Let’s make a classic ping-pong demo. We’ll send ping, and our process should pong it back.

xlerb[1]> bye
xlerb[0]> [ whatever receive drop ! :pong send recurse ] spawn
xlerb[1]> ! self :ping send whatever receive .s flush
:pong

It worked! But hang on, it’s a ‘anything’-pong server:

xlerb[1]> ! self :foobar send whatever receive .s flush
:pong

Well that’s no good! We need to narrow down what we’re receiving. Let’s see what this whatever is:

xlerb[1]> whatever .
[ [ : -> ; ] ]

So whatever pushes a quotation, which defines one word ‘->, that does nothing. Crucially though,-> is executed _on the stack of the message that's received_. Since we can [define patterns]() for matching on the state of the stack, we can use this to handle different messages. Let's write the ->` definitions ourselves, and write multiple patterns:

xlerb[1]> [
  [ 
    : :ping -> ! :pong send drop ; 
    : -> flush ; 
  ] receive recurse 
] 

This is much neater! We’re defining clauses for the word ->: one that replies back if it’s :ping, and one for anything else which just flushes out the stack.

xlerb[1]> :spawn .s
#PID<0.1.0>
! :ping send whatever receive .s flush 
:pong
xlerb[1]> :something-else whatever receive flush
xlerb[0]> :something-else whatever receive flush

Aside: Case Statements

Case statements work in just the same way, except they match on the current stack, not an incoming message. Suppose a work returns either :ok then a number, or :error on the stack:

xlerb[0]> get-score [ 
  : _ :ok -> "the score is " write . "!" write cr ; 
  : :error -> drop "failed to fetch score" puts ;
 ] case
The score is 10!
xlerb[1]>