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

Tutorial: Actors And Messages

This tutorial introduces multiple processes, message sends, and message-keyed state transitions.

Read Tutorial: Hello first if the basic Main process shape is unfamiliar.

Process Roles

examples/actor_ping.str has two processes:

  • Main, the entry process;
  • Worker, a process spawned by Main.

Main starts the worker, sends Ping, and stops. Worker receives Ping, emits output, transitions to Handled, and stops.

Worker Message Type

enum WorkerMsg {
    Ping,
}

This says Worker accepts one message: Ping.

Main can send that message after spawning Worker:

let worker: ProcessRef<Worker> = spawn Worker;
send worker Ping;
return Stop(state);

worker is an immutable process reference for the spawned runtime instance. The order matters in this source surface. Sending through a reference before it is bound is rejected.

Worker State Type

enum WorkerState {
    Idle,
    Handled,
}

Worker starts in Idle:

fn init() -> WorkerState ! [] ~ [] @det {
    return Idle;
}

After handling Ping, it stops in Handled:

fn step(state: WorkerState, Ping) -> ProcResult<WorkerState> ! [emit] ~ [] @det {
    emit "worker handled Ping";
    return Stop(Handled);
}

The transition is a whole state replacement. Handled is the new state value.

Multiple Messages

examples/actor_sequence.str extends the pattern with two worker messages:

enum WorkerMsg {
    First,
    Second,
}

When a process accepts multiple messages, each message must resolve to one step clause. Explicit patterns handle named variants, and _ handles the remaining accepted variants:

fn step(state: WorkerState, First) -> ProcResult<WorkerState> ! [emit] ~ [] @det {
    emit "worker handled First";
    return Continue(SawFirst);
}

fn step(state: WorkerState, _) -> ProcResult<WorkerState> ! [emit] ~ [] @det {
    emit "worker handled Second";
    return Stop(Done);
}

Missing coverage, duplicate explicit patterns, duplicate wildcard patterns, and wildcards that cannot cover any remaining message are rejected.

Continue Versus Stop

Continue(SawFirst) means:

  • replace the worker state with SawFirst;
  • keep the worker running;
  • allow later queued messages to be handled.

Stop(Done) means:

  • replace the worker state with Done;
  • terminate the worker normally.

The runtime trace records both steps with message IDs and state IDs.

Message Payloads

examples/actor_payloads.str adds a typed payload to a worker message:

record Job {
    phase: JobPhase,
}

enum WorkerMsg {
    Assign(Job),
}

The sender constructs one immutable payload value:

send worker Assign(Job { phase: Ready });

The receiver binds that value in a step parameter pattern:

fn step(state: WorkerState, Assign(job: Job)) -> ProcResult<WorkerState> ! [emit] ~ [] @det {
    emit "worker assigned job";
    return Stop(WorkerState { job: job });
}

The payload binding is immutable and local to the transition. Mantle executes typed message IDs; payload values travel in runtime message envelopes and appear in traces as payload metadata on the stable Assign message label.

Process references can also travel as typed payloads:

enum WorkerMsg {
    Work(ProcessRef<Sink>),
}

send worker Work(sink);

The receiver can use the immutable payload binding as a send target:

fn step(state: WorkerState, Work(reply_to: ProcessRef<Sink>)) -> ProcResult<WorkerState> ! [send] ~ [] @det {
    send reply_to Done;
    return Stop(state);
}

Mantle routes that send by the transported runtime process ID and admitted target process ID. The source binding name is trace and diagnostic metadata, not the runtime dispatch key.

Multiple Instances

examples/actor_instances.str spawns two runtime instances of the same process definition:

let first: ProcessRef<Worker> = spawn Worker;
let second: ProcessRef<Worker> = spawn Worker;
send first Ping;
send second Ping;

first and second are separate process references. Mantle assigns each spawned worker a different runtime pid, and the trace records both messages and both worker steps with the same process definition ID but different process instance IDs.

Run The Actor Examples

cargo build

cargo run -p strata --bin strata -- check examples/actor_ping.str
cargo run -p strata --bin strata -- build examples/actor_ping.str
cargo run -p mantle-runtime --bin mantle -- run target/strata/actor_ping.mta

cargo run -p strata --bin strata -- check examples/actor_sequence.str
cargo run -p strata --bin strata -- build examples/actor_sequence.str
cargo run -p mantle-runtime --bin mantle -- run target/strata/actor_sequence.mta

cargo run -p strata --bin strata -- check examples/actor_match.str
cargo run -p strata --bin strata -- build examples/actor_match.str
cargo run -p mantle-runtime --bin mantle -- run target/strata/actor_match.mta

cargo run -p strata --bin strata -- check examples/actor_instances.str
cargo run -p strata --bin strata -- build examples/actor_instances.str
cargo run -p mantle-runtime --bin mantle -- run target/strata/actor_instances.mta

cargo run -p strata --bin strata -- check examples/actor_payloads.str
cargo run -p strata --bin strata -- build examples/actor_payloads.str
cargo run -p mantle-runtime --bin mantle -- run target/strata/actor_payloads.mta

cargo run -p strata --bin strata -- check examples/actor_reply.str
cargo run -p strata --bin strata -- build examples/actor_reply.str
cargo run -p mantle-runtime --bin mantle -- run target/strata/actor_reply.mta

For actor_sequence, the trace should show Worker dequeuing First, stepping with Continue, then later dequeuing Second and stepping with Stop. actor_match should show the same typed transition behavior from the whole-body match msg authoring form.