Lowent Manual←↑→

25 Actors — living with state, by messages

What to know first

chapter 15, Effects · state and panic are effects written without capabilities
chapter 19, Ownership · once ownership is handed over, the sender cannot use the value
chapter 20, Allocators and fixed memory · an allocator is an actor satisfying a trait

Looking back

How was chapter 20′s bump allocator made, and how was it called? Where did its cursor live?

A. It was made with spawn actor allocs.bump_bytes and called with send a reserve 3. The cursor lived in the actor’s state, and the outside reached that state only through messages. This chapter covers actors properly.

The need for this chapter, and its context

Part VII covers places where several things progress together. Most concurrency defects come from several flows touching the same state together. Locks can prevent that, but a forgotten lock or a wrong order is silently wrong. Actors approach from the other side. State is kept only inside the actor, and messages are handled one at a time. Then there is simply no way to touch that state concurrently. Part VII opens with actors because this shape is already used throughout the language, as in the standard library’s allocators.

By the end of this chapter

You will learn to declare an actor with actor … do state do … end … end, make one with spawn actor, and send messages with send. You will see that ops inside actors follow the fn/proc rules too, and how to carry values in messages and hand over ownership. You will also see mailboxes filled with spawn send and emptied later with drain, restarting crashed actors with failure restart, and build profile, which decides where actors may be used.

The questions this chapter answers

  1. Isn’t calling an actor just a function call in the end?

25.1 Declare, make, talk#

examples/ch25/counter.low

module counter_demo .
rem run: use_counter

actor counter do
  state do
    value u64 .
  end

  proc inc output u64 . effects state .
  do
    set value (add value 1) .
    return value .
  end

  fn get output u64 .
  do
    return value .
  end
end

proc use_counter output u64 . effects state .
do
  var c counter be spawn actor counter .
  let a u64 be send c inc .
  let b u64 be send c inc .
  return send c get .
end

Output

$ lowentc --run use_counter counter.low
use_counter() = 2

An actor handles messages one at a time. So its state is never touched concurrently. That is why there is no need to take locks by hand.

Think of a bank with a single counter. Customers (messages) queue, and the clerk (the actor) serves one customer at a time. The vault (the state) is behind the counter, so no customer can open it directly.

  senders                        mailbox (queue)          actor counter
                                                     ┌──────────────────────┐
  send c inc  ───────▶ ┌─────┬─────┬─────┐           │  handling now: inc   │
  send c inc  ───────▶ │ inc │ inc │ get │ ────────▶ │                      │
  send c get  ───────▶ └─────┴─────┴─────┘ one by one│  state: value = 1    │ ← not reachable from outside
                                                     └──────────────────────┘

Because messages leave the queue one at a time, two messages never change value at the same moment.

Writing fn for something that changes state is rejected.

examples/ch25/pure_bad.low

module pure_bad .
rem expect: E-EFFECT-PURITY

actor counter do
  state do
    value u64 .
  end

  fn inc output u64 .
  do
    set value (add value 1) .
    return value .
  end
end

Output

$ lowentc --check pure_bad.low
pure_bad.low:9:0 E-EFFECT-PURITY: a `fn` message handler WRITES the actor's STATE — and that write is visible to the NEXT message. A fn is an enforced purity contract (SPEC-003 §27): callers may memoise, reorder or elide it. Declare it a `proc` (and say `effects state`). ★ Nobody used to look here: handlers were declared with `on`, which said NOTHING about pure-vs-procedural, and the IR read that as PURE. RFC-0046 removed the bare-`op` default exactly so the 1 bit is always stated — `on` had put it back. RFC-0057
pure_bad.low:10:3 E-EFFECT-CALC: this fn is declared pure but performs `state` — make it a `proc` with `effects …`, or remove the effect

As the diagnostic says, that write is visible to the next message. If a caller remembered results or reordered calls, the answer would be wrong.

25.2 Carrying values in messages#

Values are written side by side after the message name. On the receiving side the actor itself is treated like the first parameter, and the carried values follow.

examples/ch25/args.low

module args .
rem run: tally 5 7

actor account do
  state do
    balance u64 .
  end

  proc deposit input amount u64 . output u64 . effects state .
    requires le amount 1000000 .
  do
    set balance (wrap_add balance amount) .
    return balance .
  end
end

proc tally input a u64 . input b u64 . output u64 . effects state .
  requires le a 1000000 .
  requires le b 1000000 .
do
  var acct account be spawn actor account .
  let x u64 be send acct deposit a .
  return send acct deposit b .
end

Output

$ lowentc --run tally args.low 5 7
tally(5, 7) = 12

send acct deposit a puts a into deposit’s amount. Message ops can have contracts too.

Sending a value with ownership hands the ownership over.

examples/ch25/handoff.low

module handoff .
rem expect: E-OWN-MOVED

type job u64 .

actor worker do
  state do
    done u64 .
  end
  proc take input j owned job . output u64 . effects state .
  do
    drop j .
    set done (add done 1) .
    return done .
  end
end

proc give_twice input n u64 . output u64 . effects state .
do
  var w worker be spawn actor worker .
  var j owned job be n .
  let a u64 be send w take j .
  return send w take j .
end

Output

$ lowentc --check handoff.low
handoff.low:23:0 E-OWN-MOVED: this `owned` value was already MOVED (consumed) — using it again is use-after-move, which SPEC-004 §4.8 has always called a compile error and which nothing enforced. To keep using it, either CONSUME AND PUT IT BACK (`set <name> <new value>` re-initialises the place — that is how a handle threads through a loop), or borrow it LOCALLY with `ref h`. ☞ borrowing across an OP BOUNDARY is not lowered yet (E-IR-UNSUP says so at the call site), so `f (ref h)` is not a way out today

Once j has gone out with the first message, give_twice can no longer use j. When two flows hold the same value together, races arise. When ownership travels only by message, only one party can touch the value at any moment, and a race is not so much prevented as left no place to happen.

Q. Isn’t calling an actor just a function call in the end?

A. send resembles a call in syntax, but two things differ. State is locked inside the actor, so the caller has no way to read or write its fields, and handling one message at a time is guaranteed. Also, holding something borrowed from an actor while talking to that actor again is rejected (E-BORROW-EXCL), because we would be looking at the state while the actor changes it.

25.3 Mailboxes — putting in and emptying#

send waits until handling finishes. Deliveries whose results are not needed are only put into the mailbox with spawn send.

examples/ch25/mailbox.low

module mailbox .
rem run: drive
rem run: nodrain

actor counter do
  state do
    v u64 .
  end
  proc inc output u64 . effects state .
  do
    set v (add v 1) .
    return v .
  end
  fn get output u64 .
  do
    return v .
  end
end

proc drive output u64 . effects state .
do
  var c counter be spawn actor counter .
  spawn send c inc .
  spawn send c inc .
  spawn send c inc .
  drain c .
  return send c get .
end

proc nodrain output u64 . effects state .
do
  var c counter be spawn actor counter .
  spawn send c inc .
  spawn send c inc .
  return send c get .
end

Output

$ lowentc --run drive mailbox.low
drive() = 3
$ lowentc --run nodrain mailbox.low
nodrain() = 0

drive puts inc in three times, empties the mailbox in the order put in with drain c, and then reads 3. nodrain did not empty it, so the result is 0. Messages in a mailbox are not handled by themselves.

The processor does not decide delivery times on its own for the sake of determinism. The same program must give the same answer, and the delivery time is the answer. Because a person picks where to empty, the VM and native code deliver in the same order, and the cross-check of two back ends keeps it so. schedule . empties the mailboxes of every actor at once. Fixing the mailbox size with mailbox bounded 2 . makes overflowing puts stop, and try spawn send gives the error as a value instead of stopping.

A common misconception. Every actor is its own thread

An actor is a shape of execution, not a thread. In this edition’s processor, send, spawn send and drain are delivered deterministically within one flow. What actors give is not parallelism but isolation of state. Splitting work over several flows is the job of tasks and channels (chapter 26), and computing data split up at the same time is the job of parallel loops (chapter 27).

25.4 Let it crash, then restart#

When an actor’s op panics, the actor can be restarted. Its state returns to the beginning and the message is handled again.

examples/ch25/restart.low

module restart .
rem run: recover
rem trap: escalate

actor recov do
  state do
    bad u64 .
  end
  failure restart max 3 .
  proc step output u64 . effects state panic .
  do
    if ne bad 0 . do
      panic "corrupted state" .
    end
    set bad 1 .
    return 42 .
  end
end

actor doomed do
  state do
    v u64 .
  end
  failure restart max 2 .
  proc boom output u64 . effects state panic .
  do
    panic "always fails" .
    return 0 .
  end
end

proc recover output u64 . effects state panic .
do
  var c recov be spawn actor recov .
  let x u64 be send c step .
  return send c step .
end

proc escalate output u64 . effects state panic .
do
  var c doomed be spawn actor doomed .
  return send c boom .
end

Output

$ lowentc --run recover restart.low
recover() = 42
$ lowentc --run escalate restart.low
== ir diagnostics (1) ==
0:0 E-VM-PANIC: the program called `panic` — this is an unrecoverable stop, and it is NOT a contract violation (the code chose to stop, it did not break a promise)

There are three policies — restart max <count> (that many times), never (pass upwards on the first crash) and always (restart without counting). Restarting applies only to panic. A contract violation is the program breaking a promise it wrote itself, and doing it again gives the same result, so it is not restarted. Only the actor’s own state is revived; other actors are not touched.

Code that undoes, one by one, failures where state became strange by chance is long, and long code is itself wrong. Resetting the state to the beginning is a short recovery that is always right.

25.5 Where actors may be used#

A program can state where it runs with build profile <name> .. When it does, concurrency that place cannot support is rejected at translation.

ProfileLevelUp to
freestanding0No operating system. No concurrency
embedded1Running fixed work split up (flows fixed statically)
native2Making flows and exchanging over channels
server3Actors

Table 25.1 — Profiles and the levels they open

examples/ch25/profile.low

module profile .
rem expect: E-PROFILE-LEVEL

build profile embedded .

actor counter do
  state do
    value u64 .
  end
  proc inc output u64 . effects state .
  do
    set value (add value 1) .
    return value .
  end
end

Output

$ lowentc --check profile.low
4:0 E-PROFILE-LEVEL: this module uses a CONCURRENCY FEATURE its build profile does not provide (RFC-0009 §105/§139): freestanding has no concurrency at all, embedded has a static task graph, native adds jobs/green threads, server adds actors. The level is not declared — it is COMPUTED from the constructs you used — and the profile caps it. Either raise the profile or stop using the feature

Declaring an actor alone is level 3. Without a profile, nothing is restricted — only whoever writes it takes on the promise.

25.6 Actors that hold a capability, and what state may contain#

An actor’s state may have a capability field. A capability is a mark that exists only at translation, so the field has no size at run time. In return, a rule fixes what fills it.

examples/ch25/capfield.low

module capfield .
rem run: main

rem a log book that carves 16 bytes from the fixed window for each entry
actor logbook do
  state do
    rem a capability field; it has no size at run time and is filled from the capability at the spawn site
    root cap allocator .
    entries u64 .
  end
  proc write input code u8 . output bool . effects alloc state . do
    let g option mut slice u8 . . be alloc_bytes root capacity 16 .
    guard is_some g . else return false .
    let v mut slice u8 . . be some_value g .
    set (index v 0) code .
    set entries (add entries 1) .
    return true .
  end
  fn count output u64 . do
    return entries .
  end
end

proc main input al cap allocator . output u8 . effects alloc state .
do
  var book logbook be spawn actor logbook .
  let a bool be send book write 7 .
  let b bool be send book write 9 .
  return narrow u8 (send book count) .
end

Output

$ lowentc --run main capfield.low
main() = 2

An op that did not receive the capability is refused when it spawns the same actor.

examples/ch25/mistake_capfield.low

module mistake_capfield .
rem expect: E-CAP-FORGE

actor logbook do
  state do
    root cap allocator .
    entries u64 .
  end
  proc write input code u8 . output bool . effects alloc state . do
    let g option mut slice u8 . . be alloc_bytes root capacity 16 .
    guard is_some g . else return false .
    set entries (add entries 1) .
    return true .
  end
end

rem ✘ an op that was not handed cap allocator spawns an actor with a capability field
proc sneaky output u64 . effects alloc state .
do
  var book logbook be spawn actor logbook .
  let a bool be send book write 7 .
  return 0 .
end

Output

$ lowentc --check mistake_capfield.low
mistake_capfield.low:20:0 E-CAP-FORGE: this spawns an actor whose state HOLDS a capability (`cap allocator` / `cap heap`), but the op spawning it holds no capability of that kind. A capability field means nothing at run time — it is only real because the place that creates the actor already had the right. Without that, one `spawn` would forge authority out of thin air (RFC-0112 D6). Take the capability as an input of this op

If this were allowed, one spawn line would conjure a capability out of nothing. A capability field only records the fact that “the place that created this actor already held the capability”. The standard allocators allocs.fixed_bytes and heap_bytes are ordinary actors that follow this rule (chapter 20).

This is what a state field may and may not hold.

TypeAccepted?Why
numbers · bool · option · structsyesthey are values, so they stay inside the actor
slice · mut sliceyesthis is how an allocator holds its backing bytes (allocs.bump_bytes)
cap allocator · cap heapyes — the spawning op must hold the same capabilityE-CAP-FORGE stops forging
array <count> <type>refused — E-TYPE-ARRAYfixed-length arrays are accepted only as op inputs; there is nowhere to keep the length
ref · mut_refaccepted in this edition — but using it stops the programthere is nowhere to say what it borrows; see “Common mistakes” below

Table 25.2 — Types of actor state fields

25.7 Designing with actors — a transfer between two accounts#

This section gathers the pieces so far into one design. Each account is an actor, and a transfer is an op that talks to the two actors in turn.

examples/ch25/transfer.low

module transfer .
rem run: move 30
rem run: move 80

enum bank_error do
  insufficient .
end

actor account do
  state do
    balance u64 .
  end

  rem add to the balance --- it changes state, so it is a proc
  proc deposit input amount u64 . output u64 . effects state .
    requires le amount 1000000 .
  do
    set balance (wrap_add balance amount) .
    return balance .
  end

  rem if short, leave the state untouched and return the error as a value
  proc withdraw input amount u64 . output result u64 bank_error . effects state .
    errors insufficient .
  do
    guard le amount balance . else return error insufficient .
    set balance (sub balance amount) .
    return ok balance .
  end

  fn peek_balance output u64 .
  do
    return balance .
  end
end

rem a transfer between two actors --- deposit only when the withdrawal succeeded
proc move input amount u64 . output u64 . effects state .
  requires le amount 1000 .
do
  var src account be spawn actor account .
  var dst account be spawn actor account .
  let seed u64 be send src deposit 50 .
  let r result u64 bank_error be send src withdraw amount .
  guard not (is_error r) . else return add (mul (send src peek_balance) 1000) (send dst peek_balance) .
  let t u64 be send dst deposit amount .
  return add (mul (send src peek_balance) 1000) (send dst peek_balance) .
end

Output

$ lowentc --run move transfer.low 30
move(30) = 20030
$ lowentc --run move transfer.low 80
move(80) = 50000

The errors insufficient . of withdraw has no condition on purpose. The fourth item of “Common mistakes” below explains why.

25.8 Common mistakes#

Counter-example. Putting messages into a bounded mailbox without draining it

examples/ch25/mistake_bounded.low

module mistake_bounded .
rem trap: burst

actor counter do
  state do
    value u64 .
  end
  mailbox bounded 2 .
  proc inc output u64 . effects state .
  do
    set value (add value 1) .
    return value .
  end
  fn get output u64 .
  do
    return value .
  end
end

proc burst output u64 . effects state .
do
  var c counter be spawn actor counter .
  spawn send c inc .
  spawn send c inc .
  rem ✘ the mailbox holds two, yet a third is put in before draining
  spawn send c inc .
  drain c .
  return send c get .
end

Output

$ lowentc --run burst mistake_bounded.low
== ir diagnostics (1) ==
0:0 E-VM-MAILBOX-FULL: the actor's bounded mailbox is full — `spawn send` would exceed `mailbox bounded N` pending messages (drain it first, or raise N)

mailbox bounded 2 . promises “at most two pending messages”. The third spawn send overflows and stops with E-VM-MAILBOX-FULL. To handle it instead of stopping, send with try spawn send. The overflow then comes back as a result, and you can drain and send again.

examples/ch25/bounded_fixed.low

module bounded_fixed .
rem run: burst

actor counter do
  state do
    value u64 .
  end
  mailbox bounded 2 .
  proc inc output u64 . effects state .
  do
    set value (add value 1) .
    return value .
  end
  fn get output u64 .
  do
    return value .
  end
end

proc burst output u64 . effects state .
do
  var c counter be spawn actor counter .
  spawn send c inc .
  spawn send c inc .
  rem if it cannot be put in, receive the error as a value instead of stopping --- drain, then put it in again
  let r result u64 mailbox_full be try spawn send c inc .
  if is_error r . do
    drain c .
    spawn send c inc .
  end
  drain c .
  return send c get .
end

Output

$ lowentc --run burst bounded_fixed.low
burst() = 3

Counter-example. Sending a message the actor does not handle

examples/ch25/mistake_unknownmsg.low

module mistake_unknownmsg .
rem expect: E-IR-UNDEF

actor counter do
  state do
    value u64 .
  end
  proc inc output u64 . effects state .
  do
    set value (add value 1) .
    return value .
  end
end

proc use_counter output u64 . effects state .
do
  var c counter be spawn actor counter .
  rem ✘ `counter` does not handle a `dec` message
  return send c dec .
end

Output

$ lowentc --check mistake_unknownmsg.low
mistake_unknownmsg.low:19:0 E-IR-UNDEF: send names a message this actor does not handle (the handler is looked up IN THE RECEIVER'S TYPE — it used to be found by BARE NAME, so two actors with a handler of the same name silently shared one)

A message name is looked up in the receiving actor’s type. counter has no dec, so this is E-IR-UNDEF. As the diagnostic’s note tells, lookup was once by bare name, and two actors with a handler of the same name silently shared one.

Counter-example. Reading an actor’s state field from outside

examples/ch25/mistake_peekstate.low

module mistake_peekstate .
rem expect: E-ACTOR-FIELD

actor counter do
  state do
    value u64 .
  end
  proc inc output u64 . effects state .
  do
    set value (add value 1) .
    return value .
  end
end

proc peek output u64 . effects state .
do
  var c counter be spawn actor counter .
  let a u64 be send c inc .
  rem ✘ reads the actor's state field from outside --- this should be blocked, but passes in this edition
  return field c value .
end

Output

$ lowentc --check mistake_peekstate.low
mistake_peekstate.low:20:0 E-ACTOR-FIELD: this reads a STATE FIELD of an actor from outside it. An actor's state lives inside the actor and nowhere else (§10.2): the only door is a message (`send a <op> …`). If the state were readable from outside, the isolation that makes actors safe without locks — one message at a time — would not hold, and the reader would see a value between two messages. Add an op to the actor that returns what you need

The first promise of this chapter was “the state lives only inside the actor and cannot be touched directly from outside”. field c value tries to go around that door and is refused with E-ACTOR-FIELD. If state were readable from outside, handling one message at a time would buy nothing — the reader would see a value between two messages. If you need the state, give the actor a read message such as get and ask with send c get.

Counter-example. Using a state field in the error condition of a message op

examples/ch25/mistake_errorsstate.low

module mistake_errorsstate .
rem expect: E-ERRORS-STATE

enum bank_error do
  insufficient .
end

actor account do
  state do
    balance u64 .
  end

  proc deposit input amount u64 . output u64 . effects state .
    requires le amount 1000000 .
  do
    set balance (wrap_add balance amount) .
    return balance .
  end

  rem ✘ the error condition reads the state field `balance` --- on exit it is read again, after the balance has shrunk
  proc withdraw input amount u64 . output result u64 bank_error . effects state .
    errors insufficient gt amount balance .
  do
    guard le amount balance . else return error insufficient .
    set balance (sub balance amount) .
    return ok balance .
  end
end

proc take30 output u64 . effects state .
do
  var a account be spawn actor account .
  let seed u64 be send a deposit 50 .
  let r result u64 bank_error be send a withdraw 30 .
  guard not (is_error r) . else return 1 .
  return ok_value r .
end

var fee u64 be 5 .

rem ✘ the same holds outside an actor --- a module `var` and a `mut` argument can both be changed by the body, so neither may stand in an error condition
proc charge input amount u64 . input log mut slice u8 . . output result u64 bank_error . effects state .
  errors insufficient lt amount fee .
do
  set fee 0 .
  return ok amount .
end

Output

$ lowentc --check mistake_errorsstate.low
mistake_errorsstate.low:42:0 E-ERRORS-STATE: the condition of this `errors` clause names something whose value can DIFFER between entry and exit — an actor state field, a module `var`, or a `mut` parameter. An `errors` condition is read on the values the op was ENTERED with (canon §6.4.2): the clause says what the CALLER got wrong, and what the caller handed over is all it can be blamed for. A name the body may change cannot carry that meaning — read on exit it accuses the op of owing an error it never owed. Write the condition over the inputs that do not change (and module constants), and guard on the changing thing inside the body
21:0 E-ERRORS-STATE: the condition of this `errors` clause names something whose value can DIFFER between entry and exit — an actor state field, a module `var`, or a `mut` parameter. An `errors` condition is read on the values the op was ENTERED with (canon §6.4.2): the clause says what the CALLER got wrong, and what the caller handed over is all it can be blamed for. A name the body may change cannot carry that meaning — read on exit it accuses the op of owing an error it never owed. Write the condition over the inputs that do not change (and module constants), and guard on the changing thing inside the body
mistake_errorsstate.low:42:0 E-NAME-BUILTIN: a PARAMETER takes the name of a builtin op — inside this op the name now means two things, and which one it means decides how the sentence is bracketed (RFC-0046 P1). Rename the parameter

errors insufficient gt amount balance . was written to mean “this error when withdrawing more than the balance”. But an errors condition is read on the values the op was entered with (canon 6.4.2). errors stands on the same side as requires — it says what the caller got wrong, and what the caller did is hand things over. So a name the body can change (a state field, a module var, a mut parameter) cannot stand in the condition, and translation refuses it with E-ERRORS-STATE. Read on exit, a successful run would accuse itself: the balance dropped from 50 to 20, so on exit gt 30 20 is true, which amounts to “the condition holds, yet the error was not returned”. Write the error condition over the inputs, and let the guard in the body judge the state — as transfer.low above does with a bare errors insufficient ..

Counter-example. Keeping a borrow in a state field

examples/ch25/mistake_reffield.low

module mistake_reffield .
rem expect: E-ACTOR-STATE-REF

actor holder do
  state do
    rem ✘ keeping a borrow in state; there is no place that says what it borrows
    r ref u64 .
  end
  fn peek output u64 . do
    return deref r .
  end
end

proc peek_it output u64 . effects state .
do
  var h holder be spawn actor holder .
  return send h peek .
end

Output

$ lowentc --check mistake_reffield.low
7:0 E-ACTOR-STATE-REF: an actor STATE field may not be a borrow (`ref` / `mut_ref`). A borrow may not outlive what it borrows (§8.4.1), and a state field lives as long as the actor — there is nowhere here to say what it borrows. Keep a VALUE in state (copy it in), or keep a slice the actor was handed and owns for its lifetime
mistake_reffield.low:17:0 E-ACTOR-UNINIT: this is the FIRST message sent to an actor that was just spawned, and the handler READS a state field that holds a slice without setting it. A fresh actor's state is all zeroes, and zero is not a slice: the VM stops (`len needs a slice`) while native quietly answers `none` — the two back ends disagree, which means one of them is lying. Send the message that sets it up first (the one whose handler `set`s that field, typically `init`)

A borrow (ref) cannot outlive what it borrows (chapter 12). An actor’s state stays for as long as the actor lives, so the field has nowhere to say what it borrows. So the declaration is refused with E-ACTOR-STATE-REF. Until 2026-09-16 the declaration was accepted and the actor spawned with the field empty; only at deref r did the VM stop with E-VM-TYPE and native code with a panic. Keep values in state instead of borrows, and if a value is large, keep a slice the actor owns for its lifetime.

A common misconception. A restarted actor continues from the state just before it blew up

examples/ch25/restart_resets.low

module restart_resets .
rem run: three_incs

actor counter do
  state do
    value u64 .
  end
  failure restart max 3 .
  proc inc output u64 . effects state panic .
  do
    rem blows up once, when the value is 2
    if eq value 2 . do panic "at two" . end
    set value (add value 1) .
    return value .
  end
end

proc three_incs output u64 . effects state panic .
do
  var c counter be spawn actor counter .
  let a u64 be send c inc .
  let b u64 be send c inc .
  rem the third message blows up, the state goes back to 0, and this message is processed again
  return send c inc .
end

Output

$ lowentc --run three_incs restart_resets.low
three_incs() = 1

After two incs the value is 2, and the third message blows up. A restart puts the state back to the beginning (0) and processes that message again, so the answer is 1, not 3. The state just before the failure may be the very reason for the failure, so it is not trusted. Keep values that must not be lost outside the actor — in another actor or a file — and let the restarted actor read them.

A common misconception. Values of the same actor type share their state

examples/ch25/separate_state.low

module separate_state .
rem run: two_counters

actor counter do
  state do
    value u64 .
  end
  proc inc output u64 . effects state .
  do
    set value (add value 1) .
    return value .
  end
end

proc two_counters output u64 . effects state .
do
  var left counter be spawn actor counter .
  var right counter be spawn actor counter .
  let a u64 be send left inc .
  let b u64 be send left inc .
  rem `right` has its own state --- the result is 1
  return send right inc .
end

Output

$ lowentc --run two_counters separate_state.low
two_counters() = 1

spawn actor counter twice makes two states. Incrementing left twice leaves right counting from 1. An actor type is a blueprint; a new state is made on every spawn. If many parties must see the same value, keep one actor that holds it and have everyone talk to that actor.

25.9 This chapter’s syntax at a glance#

ShapeMeaningWhy
actor counter do state do value u64 . end … enddeclare a unit of execution that encloses statethere is no way to touch the state concurrently
proc inc … effects state . · fn get …a message that changes state · one that only readsfn/proc rules unchanged — a fn that writes is E-EFFECT-PURITY
var c counter be spawn actor counter .create one actor (state starts at 0)each spawn has its own state
send c inc · send acct deposit asend and wait until processed · carry a valueactor first — a message is an op taking the actor as first parameter
spawn send c inc . · drain c . · schedule .put in the mailbox · drain that actor’s mailbox · drain alla person picks the delivery point — determinism
mailbox bounded 2 . · try spawn sendmailbox size · receive overflow as a valueoverflow stops, or becomes a result
failure restart max 3 . · never · alwaysrestart a panicked actor from its initial statewhen used up, the failure goes upward
build profile server .where actors (level 3) may be usedonly those who write it take on the promise
state do root cap allocator . … enda capability field — size 0 at run timeE-CAP-FORGE if the spawning op lacks that capability

Table 25.3 — Actor syntax — shape · meaning · why it looks this way

Recap

An actor locks its state inside and handles messages one at a time. It is made with spawn actor and called with send, which waits, and message ops follow the fn/proc rules. Messages carry values, and sending an owned value hands over ownership. spawn send only puts messages into the mailbox, and drain and schedule empty it. failure restart resets only the state of an actor that panicked, and once exhausted passes the failure upwards. build profile decides where actors may be used.