µ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
:
.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.