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

The locus

α — What is a locus, and why is everything one?

The locus is the single structural primitive Aperio gives you. Apps are loci. Services are loci. Handlers, caches, pools, queues, namespaces, schedulers, libraries — all loci. There is no class, no module, no actor, no package. There’s one shape, and you compose it.

Anatomy

A locus is a typed unit with up to seven kinds of members. None are required; you opt in to the ones you need.

@form(vec)                              // optional: form lowering
locus Matchmaker : projection chunked,  // optional: annotations
                   schedule cooperative {

    params {                            // declared state
        target_size: Int = 4;
    }
    contract {                          // typed surface across the boundary
        expose pending_count: Int;
    }
    bus {                               // typed pub/sub interface
        subscribe JoinQueue as on_join;
        publish   MatchReady;
    }
    capacity {                          // bounded storage discipline
        heap waiting of Player;
    }

    birth()       { /* setup */ }       // lifecycle: 5 methods
    accept(c: T)  { /* on child arrival */ }
    run()         { /* steady state */ }
    drain()       { /* prepare to dissolve */ }
    dissolve()    { /* teardown */ }

    on_failure(c: T, err: Error) { ... }    // recovery policy

    mode bulk(...)       -> ... { ... }      // optional: kernel projections
    mode harmonic(...)   -> ... { ... }
    mode resolution(...) -> ... { ... }

    closure books_balance {              // structural invariants
        sum(intent.pnl) ~~ sum(book.pnl) within 0.05d;
    }

    fn on_join(p: Player) { ... }        // member functions
}

You’ll never use all of these in one locus. Most loci use three or four. The point of the surface isn’t completeness — it’s that every distinct kind of structural commitment a unit can make has a syntactic home. State goes in params. What crosses the parent ↔ child boundary goes in contract. What goes over the bus goes in bus. Bounded storage goes in capacity. Failure policy goes in on_failure. Invariants that must hold across the locus’s lifetime go in closure. Each commitment is declared, not inferred from code.

Walking through the surface

params is the locus’s state. It’s both initialized at construction (Matchmaker { target_size: 8 }) and mutated at runtime (self.target_size = 6; inside a method). Aperio collapses the parameter/state distinction the way Ruby collapses parameter/@foo-instance-variable. There is no separate state block.

contract declares what crosses the boundary between this locus and its parent. expose is what the parent can read; consume is what the parent must provide (when this locus is itself the parent of children that expose the named field). The contract is the only surface the parent sees — internal state not exposed is invisible.

bus declares typed pub/sub. subscribe Topic as handler binds an incoming message stream to a handler function on the locus body. publish Topic authorizes outbound sends on that topic via Topic <- payload;. Subjects are first-class typed declarations (topic JoinQueue { payload: Player; }), not strings.

capacity declares bounded storage other than the locus’s implicit arena. pool X of T; is fixed-shape cell recycling. heap Y of T; is growable storage individually freed during the locus’s lifetime. The @form(...) annotation on the locus picks a high-level lowering — @form(vec) over a heap slot synthesizes push / pop / len methods; @form(hashmap) over a pool slot synthesizes keyed-store methods. You’ll choose between forms based on access pattern; you don’t write the storage code yourself.

Lifecycle methods are not regular fns. They’re state-machine transitions the runtime invokes:

  • birth() runs once at construction.
  • accept(c) runs when a child locus is attached (per parent policy; see the next chapter).
  • run() is the steady-state loop, if any.
  • drain() halts new work but lets in-flight finish.
  • dissolve() tears down the locus’s region.

Every locus has all five available; the compiler supplies defaults for any you omit (birth no-ops, dissolve frees the region, etc.).

on_failure(c, err) is the parent’s recovery policy when a child fails. The handler chooses among restart, quarantine, bubble, dissolve, or absorbs by returning normally. (Failure itself is covered in detail in The two failure channels.)

mode bulk / mode harmonic / mode resolution are three named projections of the same kernel computation — vectorized bulk processing, per-class projection, single-decision resolution. A locus declares whichever subset it operates in; they share state through the same arena. You’ll rarely declare all three.

closure is a structural invariant that must hold across some declared epoch (e.g., every dissolve, every tick, every duration window). The ~~ operator means “approximately equal within tolerance.” A closure that fails routes through on_failure like any other structural failure.

Closures also serve as named structural-failure types that member functions can fire inline. The epoch inline variant declares a closure whose only firing mode is explicit violate NAME from a method body; an optional captures: f1, f2 clause names locus state to snapshot into the violation payload. This shape is the bridge between the value channel and the structural channel — covered in detail in The two failure channels. (Spec reference: F.27 in spec/design-rationale.md.)

locus vs type

If you’ve gotten this far you may be wondering when to use a locus vs Aperio’s other declarative primitive, type.

type Player { id: String; name: String; }

type is pure shape. A record. No lifecycle, no flow, no state machine, no bus participation. Construct, pass around by value, compare. The bus carries types as payloads. Your locus’s params are typed by types.

type and locus are not parallel categories — they’re points on a gradient. A type is a locus in proto-form: shape declared, but no flow attached yet. If the thing you’re modeling starts as data and grows lifecycle (a Cache that’s loaded / probed / evicted; an Order that’s submitted / filled / cancelled), you don’t bolt methods onto the type — you promote it to a locus. There is no third primitive.

The one-tower rule

The deepest commitment Aperio makes about modeling is this:

Every named quantity in your model must be assignable to exactly one locus in one locus tower.

State that “lives between” loci — a global variable, a shared mutable buffer, a side-channel cache nobody owns — is a signal of modeling error, not a framework gap. When the language seems to resist where you want to put a piece of state, the productive move is to find the locus that should own it, not to invent a workaround.

This rule exists because every other guarantee Aperio makes depends on it. Wholesale region freeing at dissolve, vertical- only flow, the closure-violation channel, the deterministic cleanup cascade — all of them assume each piece of state has exactly one owning locus. When state floats, those guarantees unravel at the floating point.

The rule is also what enables the structural correspondence you saw in the intro. When the mental model says “the matchmaker holds the queue,” it’s because the queue belongs to exactly one tower. The locus surface lets you write that down directly.

Modeling — how to think in Aperio develops this rule into concrete patterns and points at a forthcoming companion library that helps you make ownership decisions explicit.

Next

The next chapter, Recursive composition, shows how loci nest inside loci, what crosses the boundary, and why flow is vertical-only — siblings never see each other directly.