µFork Tutorial

A pure actor-based concurrent machine architecture

with memory-safety and object-capability security

Introduction

uFork is a novel machine architecture based on the Actor Model of computation. Actors are stateful objects that react to message-events. Each actor has a unique address and manages its own private state. All computation occurs in the context of an actor's message-handler.

uFork is a stack-machine. When an actor begins processing a message, it starts with an empty stack. Instructions generally remove arguments from the stack and leave results on the stack for subsequent instructions. Many instructions also have an immediate argument that influences the operation. Let's take a look at the code for a simple actor (following the "service" pattern). You may find it useful to have the uFork Assembly Language manual open for reference.

svc_beh:                    ; _ <- cust,num
    msg -1                  ; num
    dup 1                   ; num num
    push 1                  ; num num 1
    alu sub                 ; num num-1
    alu mul                 ; reply=num*(num-1)
    msg 1                   ; reply cust
    actor send              ; --
    end commit

This actor expects a message containing a cust,num pair. It calculates num*(num-1) and sends that reply to the cust actor. Comments at the end of each line depict the contents of the stack after that instruction. Note that the reply message is not actually sent until the message-handling transaction is committed.

Pair-List Indexing

The ordered pair is the most commonly used data-structure in uFork, so a more-detailed explanation is warranted. A list is a linked sequence of pairs. Instructions like msg, state, and nth have an immediate index argument (n) to succinctly designate parts of a pair-list.

  • Positive n designates elements of the list, starting at 1
  • Negative n designates list tails, starting at -1
  • Zero designates the whole list/value
0-->[1,-1]-->[2,-2]-->[3,-3]--> ...
     |        |        |
     V        V        V

If the index is out-of-bounds, the result is #? (undefined). Pair-list indexing is used to access parts of the message in the preceeding example. The following code fragment demonstrates several pair-manipulation instructions.

    push 3                  ; 3
    push 2                  ; 3 2
    push 1                  ; 3 2 1
    pair 2                  ; 1,2,3
    nth -1                  ; 2,3
    part 1                  ; 3 2

In our comment notation, each comma denotes a pair. Note that comma groups right-to-left, so a,b,c means (a,(b,c)).

Boot Behavior

At start-up, a single actor is created with boot behavior. This actor receives a message containing system-device capabilities. The boot actor is responsible for creating the initial system configuration and distributing device capabilities to actors that need them.

uFork Assembly-Language is organized into modules. A module can import symbols exported by other modules. If a module exports a boot symbol, it can provide the behavior for the boot actor. Here is a complete example using svc_beh:

Open in Playground

.import
    dev: "https://ufork.org/lib/dev.asm"

svc_beh:                    ; _ <- cust,num
    msg -1                  ; num
    dup 1                   ; num num
    push 1                  ; num num 1
    alu sub                 ; num num-1
    alu mul                 ; reply=num*(num-1)
    msg 1                   ; reply cust
    actor send              ; --
    end commit

boot:                       ; _ <- {caps}
    push 7                  ; num=7
    msg 0                   ; num {caps}
    push dev.debug_key      ; num {caps} debug_key
    dict get                ; num cust=debug_dev
    pair 1                  ; cust,num
    push #?                 ; cust,num data=#?
    push svc_beh            ; cust,num data code=svc_beh
    actor create            ; cust,num svc=svc_beh.#?
    actor send              ; --
    end commit

.export
    boot

The boot message is a dictionary of device capabilities. The dev.asm module exports a set of keys used to retrieve specific capabilities from this dictionary. The .import directive specifies a dev namespace prefix for symbols imported from the dev.asm module.

A capability is the address that designates a specific actor and confers the ability to send messages to that address. Devices capabilities are indistinguishable from actor capabilities, but are implemented directly by the machine, rather than in code.

In the example program, the boot actor defines num as 7 and cust as the debug device. A new stateless actor with svc_beh for code is created, and sent a cust,num message. Then the boot actor ends the transaction with a commit.

The svc actor receives the cust,num message, computes num*(num-1) = 42, and sends this reply to the debug device. If you run this example in the uFork Playground, the debug device will display +42 in the I/O panel.