Actors and Memory

Five classes of memory

The runtime accounts for memory in five classes, and knowing which class a thing belongs to tells you who owns it and when it goes away:

  1. runtime image — immutable VM code, native providers, and static tables;
  2. runtime RAM — scheduler, allocator, registries, stacks, I/O state, actor descriptors;
  3. mailboxes — runtime-owned serialized letters, with per-actor and total budgets;
  4. actor heaps — actor-local dynamic language values;
  5. Mach stone — immutable images holding bytecode, constants, text, shapes, and function descriptors.

Platform ledgers add the rest of what a target must budget: image views and bindings, stacks, GC scratch, alignment, DMA/audio/video buffers, asset staging, reserved memory, and free headroom.

Three regions hold values, and there are no others

The five classes above are how memory is accounted. Where a value can live is a shorter list, and it is exactly three places:

regionholdsgrows
the runtime stone poolthe constants C itself needsnever
Mach poolsthe constants of compiled codenever, once written
actor heapseverything elsewith the actor

The runtime stone pool is a fixed array, seeded before actor 0 exists and never grown. Indices 0-127 are the ASCII characters; the rest are the constant keys the C runtime refers to by name. So s[i] on a small codepoint is: read the codepoint, index the table. No hash, no probe, no lock, no allocation.

Mach pools are stone, and there are many of them — each brought along by whatever modules reference it. They are constants for bytecode, and text is deduplicated within a pool and never across pools.

Actor heaps are everything else: values a program builds while it runs, collected by that actor’s own collector when it stops referring to them.

Every actor owns its heap, its frames and closures, its module return values, and its profiling observations. The heap grows and shrinks with the actor and is released when the actor stops. A pool, by contrast, may be referred to by any number of actors, and because a pool is made from a link group rather than from a single file, how much code shares one is a build decision — a development build often maps one pool per unit, while an optimized build may put a whole executable closure in one.

So an actor’s memory is:

actor heap ──▶ its own dynamic values
           └─▶ references into the Mach pools its realization named
           └─▶ references into the runtime stone pool

Everything shared is immutable and counted once; everything counted per-actor is genuinely that actor’s. That is what makes a footprint measurable, and what lets a constrained target budget one.

Nothing is shared across actors that is not immutable, and nothing is deduplicated across regions. Text from two modules’ pools may be two different objects with the same content, and that is expected rather than a missed optimization. Comparison is by content and it is cheap: stone text caches its hash in its header, so two copies from different pools carry the same hash and a comparison is hash, then length, then bytes. That fast path is gated on the text being stone — antestone text uses the same header field for its letter count.

Views, bindings, and references

Two small structures connect an actor to its images.

A MachImageView is owned by the image provider. It holds the validated header, profile, ABI, section bounds, and placement base — one constant-sized view per opened physical placement, shared by every pin of it.

A PoolBinding is owned by the VM. It resolves an image’s declared cross-image imports to image-and-entry pairs using the realization’s binding rows, and holds whatever key, shape, or reference translation the profile needs. Its size is proportional to the cross-image imports alone, because references within an image were resolved at finalization. A single-image executable needs only its placement base. Bindings with identical encoded rows are shared.

Function objects on the actor heap hold profile-defined references to code in an image: a flat desktop profile can use a direct host pointer, while banked or segmented profiles use target-relative image references. The collector recognizes image-backed stone, preserves its placement, and traces the actor’s own dynamic objects normally.

There is no interning anywhere — not process-wide, not per-actor. Text and record shapes emitted at build time live in the images, which is where the sharing already is; text a program builds while it runs is an ordinary actor-heap value compared by content. An intern table would add a second identity mechanism to catch the one case with no reuse to find, and a process-wide one would hand an actor a pointer into another actor’s world and make a key lookup’s timing an observable channel between them.

Record shapes follow the same rule as text: inferred from code and deduplicated in a Mach image. {x: 5, y: 3} takes its shape from the pool; adding a field the code did not declare drops the shape and leaves an ordinary hashed record. Nothing makes a shape on the fly.

Turns

An actor processes one message at a time, and a turn runs to completion. A turn’s intermediate state stays private to it: other actors observe the state before the turn and the state after, so a turn is atomic by construction.

Two things can interrupt a turn, and both suspend it resumably, leaving it to be finished later — the scheduler’s timeout preemption, and, in a development build, the debugger. Because a suspended turn keeps its state in the actor rather than on a host stack, this works the same on every target.

Lifetime

The runtime reclaims and kills; it never restarts or supervises. There is no health check, no back-off, no restart policy and no supervision tree — supervision is application work, and a library someone writes. What the runtime itself owns is exactly two clocks and one rule about replies.

Turns are the anchor. Every lifetime judgement the runtime makes is about turns: how long one takes, and how long it has been since the last one. Nothing else about an actor — what it is waiting for, how much memory it holds, how many messages it has answered — enters into it.

The slow timer bounds one turn. A turn that exceeds its budget is frozen mid-turn and goes to the back of the line, resuming when its turn comes round again. A turn that keeps overrunning accrues strikes, and at the strike limit the actor is halted. So slowness is throttled first and killed only if it will not stop: an actor doing genuinely long work yields the machine to everyone else and still finishes.

The ar timer bounds idleness. Every actor carries an idle countdown that runs whenever it has nothing to do — empty mailbox, not queued, not mid-turn — and reaps it when it fires.

Waiting is not liveness. A pending delay, a park, an outstanding request, an in-flight read: none of them buys an actor time. They are all “nothing to do” and the countdown runs through every one of them. This is deliberate, and it is what makes the reap mean something: an actor blocked forever on an answer that is never coming is precisely the actor that should be reclaimed, and an actor that arranges to be woken later is one that can say so.

$unneeded is how it says so. $unneeded(fn, seconds) sets the actor’s own countdown and a callback that gets one final turn when it fires — to clean up, to re-arm, or to let the reap proceed. -1 is never. An actor that knows how long its waits should be encodes that knowledge here, at the only place that knows it, which is why no other timeout exists in the system: not on messages, not on requests, not on I/O.

A reply resolves by an answer, by the counterparty’s death, or by the holder’s own death — never by a clock. A request that carries a reply route registers a monitor on the actor it was sent to. When that actor dies — by a clean stop, by a disruption, by the reap, by slow strikes — every reply route to it fires with the death as its reason, including the routes whose letters were still sitting in the dead mailbox. Addressing an actor that is already gone answers immediately, for the same reason.

So a request has exactly three ends, and all three are events rather than durations. A caller who genuinely knows how long its own request should take may still state a deadline on that message, and it will be honoured; nothing inherits one, and there is no default. What is left over — a request outstanding against a live counterparty that simply never answers — is a protocol defect in that counterparty, not a resource leak, and $inspect reports it per actor: how many replies an actor is waiting on, and the age of the oldest. An outstanding reply is a fact you can look at rather than one a timer erases.

Coupling is the other half. A child is coupled to its overling, so an overling’s death takes its children with it, and $couple extends the same pact to any actor. A couple is a death pact — the coupler stops at the end of the turn that observes the death. Watching without dying is the monitor above, which is what a program wants when its job on hearing the news is to report it.

The defaults are build-time, not code-time. The ar seconds, the fast and slow turn budgets and the strike limit come from the recipe’s build.budgets — a constrained target reclaims sooner and tolerates less, a workstation the reverse — and the compiled-in numbers are the fallback a recipe overrides. See Recipes.

Starting an actor

Actor startup is ordinary Pit code following a plan that was computed at build time. The runner does no thinking. The shop constructed the realization and holds the decision; by the time a runner sees one, everything has been decided. So the runner does not compare artifacts against each other, re-check claims, re-authorize anything, or re-validate the start order. It reads rows and follows them.

  1. A shop hands over a realization. At cold boot, C does only enough to map the cart, load the engine image, and create the first actor.
  2. Pit opens and pins the realization’s image placements through $image, the endowment over the image provider, and asks the VM to install the declared bindings. Mapping, ROM-bank, and target-reference operations are primitives. See Booting.
  3. The runner allocates a result array in the new actor’s heap — one slot per entry, using indices assigned at finalization — then walks the realization’s rows in order. For each row it binds the value the row carries, invokes a unit’s entry and stores what it returns, or loads a compiled-in provider by key.
  4. The root unit runs last. Pit then drops bootstrap-only names and keeps the result array as ordinary actor state for the actor’s lifetime.

A binding row names a value directly: a constant the shop resolved, or the result slot of another entry. The runner holds no table of known names and constructs nothing itself — $delay and $clock are units in the realization like any other. The three entry kinds, and why an endowment is not a fourth, are in The Executable as an Array. See also Endowments.

Dependency order is calculated when the executable is built and merely realized at start. Because there is nothing to decide, there is nothing to refuse: a primitive that fails means something upstream is broken, and it disrupts like any other bug rather than taking a designed refusal path. Stopping an actor releases its descriptor, its mailbox letters, its heap, and its image pins.

The authority to start and feed actors is split, so no single holder can both create an actor and forge its identity. One primitive allocates the actor shell, applies its pre-boot fields, performs the first entry, and owns the raw message and turn hooks. A separate delivery provider performs one checked mailbox enqueue and answers delivered or dead — it is granted to the engine and the courier, and to nothing else. The public actor module a program uses holds only token display and actor-local scheduler operations; it cannot create an actor or manufacture a delivery.

Both are ordinary endowments held by whoever the realization says holds them. Nothing in the start path treats a name as private or inspects which unit is asking — an actor is one trust domain, so a value reachable by one of its units is reachable by all of them, and guarding a name inside an actor would protect nothing.

Placement

Where images actually live is a separate choice from how they were linked, and it varies by target:

  • loose desktop maps each image independently, often one image per unit while developing;
  • optimized desktop maps one or more closure-wide images;
  • flat desktop cart maps one placement group holding its images;
  • PS1 and N64 place the complete flat group in main RAM once and keep it for the process lifetime, or keep common images resident and acquire each actor root’s group for that actor’s lifetime;
  • large desktop cart partitions images into addressable groups and acquires the groups its live actors need;
  • Gameboy-class targets keep the runtime and bank-switch machinery in fixed ROM, place actor image groups in switchable ROM, and use declared work RAM and cartridge SRAM for heaps and mailboxes.

On banked targets, logical pinning and physical bank selection are distinct: pinning keeps a placement valid for an actor’s lifetime, and selection chooses which pinned placement is visible right now. Persistent values hold target-relative image references; a raw host pointer is valid while its bank is selected. A strict actor profile puts an actor’s whole image-backed closure in one group, with its heap and mailbox in mutable RAM.

Garbage collection

Each actor is collected independently. A collection pauses the one actor being collected and leaves every other actor running. The collector walks that actor’s own dynamic values and stops there: the images are immutable and live outside the heap, so a collection costs what the actor holds rather than what the program contains.

Sending values

A value sent to another actor is encoded on the way out and decoded on the way in, so each side holds its own copy. Two encodings serve two purposes:

  • wota — the fast, profile-specific encoding used within one runtime, including actor mailboxes. It writes values close to their in-memory form and follows the runtime’s value profile.
  • nota — the runtime-independent, decimal-shaped encoding used across runtime boundaries. Couriers re-encode boundary values into nota, so two runtimes built with different numeric representations agree on what a number is.

Reaching actors on other runtimes

Everything about contacting another runtime is ordinary Pit state, held by the courier — one per shop. The courier owns the runtime’s keypair, its published addresses and ports, and its transport observations, and it publishes updates to the Pit root as they change. When the root creates an actor it hands that actor an explicit local-runtime key and a courier token, which the engine keeps as the actor’s own routing state; actor creation itself returns the new actor’s opaque token to the root. Nothing about who-can-be-reached lives in C — the runtime moves bytes, and the courier decides where they go. The endowments a program uses to introduce and reach remote actors, $contact and $portal, are covered in Endowments.