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
}