Actors and Modules
ƿit source comes in two kinds, and they combine into a third thing that you run.
- A module (
.cm) returns a value. - A program (
.ce) returns nothing. It is an entry point. - An executable is one program together with the closure of modules it uses — the modules it imports, the modules those import, and so on.
- An actor is an executable running: a live unit of execution with its own heap.
Starting the same executable twice gives two actors that share no memory.
The Actor Model
ƿit is built on the actor model of computation. Each actor:
- Has its own heap — actors share no mutable state
- Processes one message per turn; a turn may be suspended at any point and resumed
- Performs its own garbage collection
- Communicates only through message passing
This isolation makes concurrent programming safer and more predictable.
Modules (.cm)
A module is a script that returns a value. The returned value is cached and frozen (made stone).
// math_utils.cm
def math = use('math/radians')
var distance = function(x1, y1, x2, y2) {
var dx = x2 - x1
var dy = y2 - y1
return math.sqrt(dx * dx + dy * dy)
}
var midpoint = function(x1, y1, x2, y2) {
return {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2
}
}
return {
distance,
midpoint
}
Key properties:
- Must return a value — it’s an error not to
- Runs once when the actor starts — every module in the executable’s closure is run a single time, in dependency order, before the program body
- Shared within the executable — if several modules import the same module, they all receive the one value it returned
- Private to the actor — a second actor running the same executable runs the modules again and gets its own values
- Return value is stone — the record itself is immutable, though values inside it are not necessarily stone themselves
- Modules can import other modules with
use(); resolution is handled by the shop.
Using Modules
def utils = use('math_utils')
var d = utils.distance(0, 0, 3, 4) // 5
Programs (.ce)
A program is a script that does not return a value. It is the entry point of an executable, and running it creates an actor.
// worker.ce
log.console("Worker started")
$receiver(function(msg) {
log.console("Received:", msg)
send(msg, {status: "ok"})
})
Key properties:
- Must not return a value — it’s an error to return
- Has access to endowments (policy-granted names starting with
$) - Runs until explicitly stopped or crashes
Endowments
An endowment is a capability that the shop’s policy grants to a module or
actor. In ƿit, endowed names begin with $. They look like variables in source,
but they are not ordinary variables: source cannot declare $ names, and the
compiler records every $name reference as an endowment requirement.
ƿit inherits this idea from Misty, where programs are endowed by the policy
machine with special names that can hold functions, constants, or actor
addresses. Misty writes those names with a ! suffix; ƿit uses a $ prefix:
log.console($self)
$receiver(function(message) {
send(message, {status: "ok"})
})
During build, the compiler analyzes the program and module graph and records
the requested endowments in the executable. Policy decides which of those
requests are allowed. If a file requests an endowment that policy does not
grant, the policy actor denies it with an error such as
policy denied $appkit_window;
that actor is not started with the denied capability. Only granted $ names
are bound into the actor’s runtime context.
Endowments are fulfilled at actor spawn, not baked into a module at build
time. Engine capabilities such as $self and $receiver are constructed by
the engine. A raw platform capability is a top-level file in an endowment
package, and the filename is the name: posix_file.c in the POSIX endowment
package fulfills $posix_file. Nothing maps one to the other. A granted raw
claim with no provider in the running binary is a spawn-time error, which makes
the target binary — rather than a separate catalog — authoritative about which
native powers actually exist.
Portable library modules consume these raw providers. std::file is a target
arm in std’s manifest: it selects internal/file_darwin,
internal/file_windows and so on, and the selected implementation claims
$posix_file, $memfs, $playdate_file, or Windows $iocp. Application
source imports the uniform surface with use('file'); policy and the runtime
fulfill the selected implementation’s claim when the actor starts. Nothing
requires you to go through it — a program that knows its machine may claim
$posix_file directly.
Actor Intrinsics
These endowments are actor capabilities.
$self
Reference to the current actor. This is a stone (immutable) actor object.
log.console($self) // actor reference
log.console(is_actor($self)) // true
$overling
Reference to the parent actor that started this actor. Child actors are automatically coupled to their overling.
send($overling, {status: "ready"})
$stop()
Stop the current actor. When called with an actor argument, stops that underling (child) instead.
$stop() // stop self
$stop(child) // stop a child actor
Important: $stop() does not halt execution immediately. Code after the call continues running in the current turn — it only prevents the actor from receiving future messages. Use return after $stop() if there is code below it.
$start(callback, program)
Start a new child actor from a program. A program can be a text literal program, or a
locator. The callback is an ordinary requestor callback,
function(reply, reason): on success reply.actor is the new child, and on failure
reply is null and reason says why.
$start(function(reply, reason) {
if (!reply) {
log.error("could not start worker:", reason)
return
}
send(reply.actor, {task: "work"})
}, "worker")
The callback must take exactly two parameters.
$delay(callback, seconds)
Schedule a callback after a delay. Returns a cancel function that can be called to prevent the callback from firing.
var cancel = $delay(function() {
log.console("5 seconds later")
}, 5)
// To cancel before it fires:
cancel()
$clock(callback)
Ask for the current time. The callback receives it as a wall-clock seconds number.
$clock(function(t) {
log.console("the time is", t)
})
$clock is asynchronous on every target, and that is why it is portable. Reading the
clock is not uniform enough to be one C surface — on some machines it is genuinely a
request that answers later — so the endowment has one shape everywhere and the per-platform
time endowment sits underneath it. $clock is the door an ordinary actor uses; the
time module is built on the raw reading below it.
$receiver(callback)
Set up a message receiver. The callback is called with the incoming message whenever another actor sends a message to this actor.
To reply to a message, call send(message, reply_data) — the message object contains routing information that directs the reply back to the sender.
$receiver(function(message) {
// handle incoming message
send(message, {status: "ok"})
})
$time_limit(requestor, seconds)
Wrap a requestor with a timeout. Returns a new requestor that will cancel the original and call its callback with a failure if the time limit is exceeded. See Parallelism for details.
var timed = $time_limit(my_requestor, 10)
timed(function(result, reason) {
// reason will explain timeout if it fires
}, initial_value)
$couple(actor)
Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between a child actor and its overling (parent).
$couple(other_actor)
$unneeded(callback, seconds)
Schedule the actor for removal after a specified time. The callback fires when the time elapses.
$unneeded(function() {
// cleanup before removal
}, 30)
$connection(callback, actor, config)
Get information about the connection to another actor. For local actors, returns {type: "local"}. For remote actors, returns connection details including latency, bandwidth, and activity.
$connection(function(info) {
if (info.type == "local") {
log.console("same machine")
} else {
log.console(info.latency)
}
}, other_actor, {})
Runtime Functions
These functions are available in actors without the $ prefix:
send(actor, message, callback)
Send a message to another actor. The message must be a record.
The optional callback receives the reply when the recipient responds.
send(other_actor, {type: "ping"}, function(reply) {
log.console("Got reply:", reply)
})
To reply to a received message, pass the message itself as the first argument — it contains routing information:
$receiver(function(message) {
send(message, {result: 42})
})
Messages are automatically flattened to plain data.
is_actor(value)
Returns true if the value is an actor reference.
if (is_actor(some_value)) {
send(some_value, {ping: true})
}
log
Channel-based logging. Any log.X(value) writes to channel "X". Three channels are conventional: log.console(msg), log.error(msg), log.system(msg) — but any name works.
Log events are sent immediately to the runtime logger. Terminal sessions can subscribe to selected channels for the actor subtree they started.
use(path)
Import a module. Use statements require use(...) to be assigned to def (for
example, def json = use('json')). See Names and the chain
for how package locators are resolved.
Example: Simple Actor System
// main.ce - Entry point
def config = use('config')
log.console("Starting application...")
$start(function(reply, reason) {
if (reply) {
send(reply.actor, {task: "process", data: [1, 2, 3]})
}
if (event.type == 'stop') {
log.console("Worker finished")
$stop()
}
}, "worker")
$delay(function() {
log.console("Shutting down")
$stop()
}, 10)
Command-line actor messages
pit program.ce is a one-shot call: the terminal starts the actor, sends an
empty record ({}), waits for one reply, renders it, and stops the actor.
With arguments, pit program.ce arg ... sends
{type: "cli", args: ["arg", ...], cwd: "..."}. An attached shell may also
include a context record. CLI-aware actors use the shared cli_args adapter
to turn this stable envelope into their typed command message.
Help is explicit: pit program.ce help [command]. Exact protocol messages use
pit --request <actor-or-program> '{"type":"..."}'; a JSON-looking ordinary
argument is still an ordinary CLI argument. pit --detached program.ce only
starts the actor and prints its id, and therefore rejects program arguments.
Machine shells can change live log subscriptions with /logs quiet,
/logs off, and /logs verbose. Quiet retains errors while hiding progress,
off unsubscribes, and verbose includes broad build/runtime logs. Mode changes
do not replay old records.
// worker.ce - Worker actor
$receiver(function(msg) {
if (msg.task == "process") {
var result = array(msg.data, function(x) { return x * 2 })
send(msg, {result: result})
}
$stop()
})
// config.cm - Shared configuration
return {
debug: true,
timeout: 30
}