Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cross-process & hot-load

Coming from Rust / C++? This is typed, versioned state shipped between processes — but without a separate .proto and a codegen step. A perspective is a serializable parameter bundle; producer and consumer share its schema because they compile from the same source. No protobuf regen, no schema drift, no handshake.

A perspective is a shippable view

Most of a locus’s state is private to its region. A perspective is the exception: a typed bundle a locus can publish across a process boundary, with a compile-time guarantee that the other side agrees on its shape.

perspective Kernel {
    params {
        scale_row:    [Decimal; 8];
        sigma_factor: Decimal;
        regime_id:    Int;
    }
    stable_when {
        return self.num_validated >= 3;
    }
    serialize_as KernelV1;
}
  • params is the payload — the schema is this type.
  • stable_when is a predicate the runtime checks before the perspective is allowed to ship — “is this data ready?” lives in the data’s own declaration, not in a publisher flag.
  • serialize_as names the wire format stably, so you can rename the identifier without breaking serialization.

A perspective is not a locus — no lifecycle, no bus block, no methods beyond stable_when. It’s a validated, serializable bundle the substrate knows how to ship.

The fitter / applier pattern

The canonical use: one process computes parameters slowly and carefully; another applies them at high frequency. Both compile from the same Kernel declaration, so the type is the protocol.

// fitter — publishes refined Kernels
locus Fitter {
    bus { publish KernelUpdates; }
    run() {
        let mut k = compute_kernel(observations);
        while !k.is_stable() { k = refine_kernel(k, more()); }
        KernelUpdates <- k;
    }
}

// applier — swaps in the latest, atomically
locus Applier {
    params { current: Kernel = default_kernel(); }
    bus { subscribe KernelUpdates as on_update; }
    fn on_update(k: Kernel) { self.current = k; }   // atomic swap; no torn read
}

The runtime guarantees the consumer-side swap is atomic — readers see the old perspective or the new one, never a half-written mix. This is also the hot-load mechanism: reconfigure a long-running service by publishing a new perspective, with full type-checking against the locally-compiled schema, no restart.

Capability profiles and substrates

The same locus + bus + perspective triple runs on more than one substrate. The native C-runtime is one; the browser runtime (hale-js) is another. A build target declares the capabilities a substrate offers:

target browser_js {
    arenas.epoch_view,
    time.monotonic, time.wallclock,
    random.csprng,
    gfx.canvas2d,
}

A program that reaches for a capability its target doesn’t offer fails at the translation boundary with a clear CAP-MISSING diagnostic — at build, not at runtime. Substrate differences are named and checked, not papered over. The locus you wrote doesn’t change between substrates; the capability profile and the transport binding do.

This is the long arc of the whole guide paying off: the same shape you met as a small program in the basics runs across processes, machines, and substrates because nothing in the shape depended on where it ran.

Next, the most specialized tier feature — Modes.