hanki

actor

stdlib/actor.hk: actor-runtime types surfaced to user code.

Every send picks up [throws actor.SendFailed] (HANKI.md §15). The runtime materialises a SendFailed at the send site: MailboxFull when a bounded mailbox is full, Died(ActorId, DeathCause) when the target has terminated, Timeout when actor.await_timeout! gave up waiting. Scheduling actor.send_after!(target.handler(args), ms) is a send site too. ActorId and DeathCause are also delivered to a supervisor's on actor_died handler.

Bounded await, a compiler special form like the send forms and no declarable fn, since Future<T> cannot cross a fn boundary):

actor.await_timeout!(f, ms) # f: Future<T>, ms: i32 -> T

It types as await f does and raises the future's effects at the call site; if no reply lands within ms milliseconds it throws SendFailed.Timeout instead. The late reply, if one ever arrives, is abandoned: the handler still ran, and only the wait was bounded. Under --deterministic the deadline is virtual, on the gate's clock, and a timeout costs no real time and replays per seed.

Delayed cast, also a compiler special form and no declarable fn:

actor.send_after!(target.handler(args), ms) # ms: i32 -> ()

It copies or moves the handler arguments now, reserves one target-mailbox slot now, and delivers the cast after ms; a negative delay clamps to zero. Scheduling has [time, throws actor.SendFailed]. A full or dead target throws synchronously, while an accepted timer cannot later fail for MailboxFull. Target death or program exit cancels the retained envelope and releases its reservation. Deterministic runs use virtual time, and timers at one deadline fire in registration order.

Arriving here for something this module does not hold

impl<T> ActorRef<T>

Identity-bearing view of an actor handle. The runtime supplies the declared actor name beside the packed slot-plus-generation id; equality on the resulting ActorId compares the packed id alone.

id

def id(self) -> ActorId

The actor's stable identity. This is the same ActorId delivered as who when the actor later dies, and two handles for actors of the same declared type still return different ids. @no-doctest: an ActorRef exists only after a runtime spawn

SendFailed

type SendFailed
  MailboxFull
  Died(ActorId, DeathCause)
  Timeout
end

Send-failure throw raised at the call site of any blocking send, statement-position fire-and-forget, or matching await.

try c.op() catch e: actor.SendFailed match e MailboxFull -> retry() Died(, ) -> crash!("peer gone") Timeout -> retry() end end

DeathCause

type DeathCause
  UncaughtThrow(string, string)
  InvariantViolation(string, Option<string>, string)
  HostPanic(string)
  ExplicitShutdown
  Gone
end

Cause of an actor's termination. Delivered to the supervisor as the second argument of on actor_died(who: ActorId, cause: DeathCause) and carried as the Died payload at send-site throws. Every variant below is one the runtime produces.

impl Display<DeathCause>

to_string

def to_string(self) -> string

The same one-line wording the runtime writes to stderr when a death escalates to root, and a supervisor that renders the cause itself and an unsupervised crash describe the same event identically.

Gone.to_string() => "gone (actor id recycled)"

impl Display<SendFailed>

to_string

def to_string(self) -> string

Names the failure a catch e: actor.SendFailed arm caught. Died reports the target's name and not its raw id: the id is a packed slot-and-generation word, noise beside a name the program chose.

Timeout.to_string() => "reply timed out"

ActorId

opaque ActorId
  raw: u64
  class: string
end

Opaque identifier for a (possibly-dead) actor. The runtime constructs ActorId values on the supervisor-delivery and send-site throw paths; user code never fabricates one.

class is the dying actor's declared name, the runtime's own actor_class for that slot, and name below reads it.

impl ActorId

name

prop name(self) -> string

The actor's declared name, Worker for a spawn Worker. A supervisor reads it to tell one dead child from another when its children are of different types.

It does not identify a child on its own: two spawn Workers under one supervisor share this name. Compare the ActorId itself to tell those instances apart. @no-doctest: the runtime constructs every ActorId; user code cannot make one to read

impl Eq<ActorId>

eq?

def eq?(self, other: Self) -> bool

Actor identity is the packed slot-plus-generation word. class is a diagnostic label and may be unavailable after slot reuse. It never participates in equality. @no-doctest: the runtime constructs every ActorId; user code cannot make one to compare

impl Hash<ActorId>

hash

prop hash(self) -> u64

Hashes the identity word Eq compares, which lets an ActorId key a Map. @no-doctest: the runtime constructs every ActorId; user code cannot make one to hash

Shutdown

type Shutdown
  Terminated
  AlreadyDead
  SelfScheduled
  TimedOut
end

Outcome of actor.shutdown!(target, kill_after=ms), read by awaiting the returned Future<Shutdown>. The deadline is optional. Every outcome is normal, and the future therefore has no throw and is freely discardable for fire-and-forget. The runtime constructs these values.

stop_all!

def stop_all!<T>(targets: List<ActorRef<T>>, kill_after: i32) -> List<Shutdown>

Stop a set of actors and report what happened to each, in the order given.

The completion boundary a request-scoped operation returns through. Each target is signalled first and every outcome is collected afterwards, which puts one deadline over the whole set. Stopping them one at a time takes kill_after per target.

TimedOut names a target that reached no shutdown safe point in time. Its shutdown request remains active and it may terminate later, and the caller has a report and no guarantee. HANKI.md section 15 has the surrounding pattern and the two limits behind that report: a target wedged in native compute on the AOT tier reaches no safe point, and a stopped actor's own children wind down asynchronously. @no-doctest: an ActorRef exists only after a runtime spawn