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

Nominal Behavior attribute

#[behavior::behavior(...)] is the single generated authoring path. It preserves an ordinary inherent impl and emits the adjacent nominal Protocol, pure Behavior, optional named send product, and optional closed child product that a careful user could write by hand.

use behavior::{Actions, BehaviorActed, Delivery, MailAddr};

struct QueryPoolProtocol;
struct QueryStarter;

#[behavior::behavior(
    addr = MailAddr,
    message = behavior::Never,
    sends = {
        query_pool: Vec<Delivery<QueryPoolProtocol>>,
    },
)]
impl QueryStarter {
    fn init(&mut self) -> BehaviorActed<Self> {
        Ok(Actions::cont().send_query_pool(/* typed delivery */))
    }

    fn receive(&mut self, _: MailAddr, message: behavior::Never) -> BehaviorActed<Self> {
        match message {}
    }
}

addr and message are required because they establish public protocol identity. The other declarations are capability-denying defaults:

  • omitted sends means NoSends;
  • omitted births means NoBirths; and
  • omitted error means Never.

Those defaults do not infer or grant an effect. Code that tries to send, create, or return an error absent from the declaration fails to type-check. An advanced author may still supply an existing concrete sends or birth type instead of a generated { ... } product; this preserves handwritten catalogue products without adding another macro path.

Named send products

For an actor System, sends = { workers: Vec<Delivery<Workers>> } generates:

  • the nominal SystemSends product with a named workers field;
  • the uninhabited lane selector SystemSendsWorkers;
  • the SystemActions extension trait, with a fluent send_workers method;
  • SendEffects, preserving each field independently in declaration order;
  • SendInput<_, SystemSendsWorkers>, used through SendEffects::send;
  • SendsFor<Event>, requiring every field to be lawful for the complete event; and
  • InterpretSends, requiring and visiting every field in declaration order.

Two fields with identical product and payload types still have different lane selector types. No recursive position, type-name search, runtime registry, or catch-all effect value is generated. An interpreter missing any constituent delivery or request implementation fails its InterpretSends bound.

Generated fluent methods reuse the same Actions value and SendInput proof: they append exactly once to their named lane while preserving existing sends, creation order, and Continue, Goto, or Stop. They do not interpret an effect or introduce a second effect representation. Existing concrete send products remain directly constructible for advanced composition code.

Closed birth products

For an actor System, this declaration:

births = {
    workers: Workers,
    query_reply: ManagedQueryReply,
    query_starter: ManagedQueryStarter,
},
creation_settlements = return_to_creator,

generates SystemChildren as the exact recursive ChildChoice produced by calling Children::child_at in that declaration order. The generated behavior's birth capability is Births<SystemChildren>. The field labels record semantic roles. It also generates SystemChildrenRoutes, with one nominally distinct typed route per role. The same route stages that role's creation and constructs ChildDelivery<Child::Protocol, Role> or ObserveEstablishedCreation<Child, Role>, so dependent effects do not repeat an untyped nonce. Values and nonces remain explicitly authored.

These names extend the macro's existing generated namespace: SystemChild, SystemChildren, SystemChildrenRoutes, and one SystemChildrenRole marker for each field (for example, SystemChildrenWorkers). As with SystemSends and its lane selectors, callers must leave those actor-prefixed names to the macro. Using the same child behavior in two fields still produces two incompatible role types; sharing a protocol cannot accidentally exchange their routes.

The same declaration also generates the SystemChild selector namespace. Its inhabited values, such as SystemChild::Workers, implement ChildRole<System, Child = Workers>. Each role also carries a structural ChildHead/ChildTail<_> position proving where that exact behavior occurs in SystemChildren, and implements ChildOccurrence<System> for the sealed running-emitter resolver. This is the Behavior-owned proof consumed by static application topology and existing heterogeneous effect products:

Application::new(System::new())
    .child(SystemChild::Workers, workers)
    .child(SystemChild::Queries, queries)

The selector contains no nonce and performs no creation. SystemChildrenRoutes remains the separate creator-local routing product used when the parent stages creation and local dependent effects.

A route is routing intent only. It is not an actor identity, proof of freshness, or successful installation fact.

Generated and handwritten child products share the same sealed occurrence product. A static runtime may implement ChildOccurrenceShape once to project any SystemChildren into its own occurrence-preserving direct-child storage. The macro emits no runtime storage or shape implementation, and equal child types at two named roles still receive distinct structural positions.

Every real birth declaration must state who owns its completed creation settlements. creation_settlements = return_to_creator makes the generated event type CreationEvent<Addr, SystemChildren, Message> and requires an authored transition with this shape:

fn creations_settled(
    &mut self,
    settlements: behavior::CreationsSettled<behavior::MailAddr, SystemChildren>,
) -> behavior::BehaviorActed<Self> {
    // Decide from the exact accepted or rejected creation settlements.
}

Use creation_settlements = retain_for_retirement when the actor has no lawful transition for those runtime results. Its birth capability becomes RetirementBirths<SystemChildren>; the interpreter retains the exact settlement as terminal source custody rather than dropping it or manufacturing a no-op callback. Omitting births, or explicitly declaring NoBirths, needs no settlement policy because no creation request can exist.

ResolveChildOccurrence<Role> maps such a generated role to the exact child and position of the concrete behavior currently being interpreted. It follows BehaviorBase through wrappers only when they preserve both the canonical protocol and complete birth algebra. Wrappers that rewrite child topology do not inherit the base role; their own ChildHead/ChildTail<_> positions remain available. The mapping is sealed and type-level, not generated runtime code.

This is Bombay's staged creation policy: Children builds typed CreateChild requests, and the interpreter must commit them before dependent same-action sends and interpreter requests. The macro does not allocate, establish, replace, choose nonces, or infer lifecycle provenance. DispatchBirth still requires one concrete EstablishChild<Occurrence, Child> implementation for every alternative. The position distinguishes duplicate occurrences without making the role a protocol identity or forcing the parent type onto ordinary creation creation-result consumers.

Exact initialization and transition results

An optional inherent init(&mut self) -> BehaviorActed<Self> becomes the initialization fold. If it is absent, initialization returns the explicit empty Actions::cont(). The required receive(&mut self, from, message) -> BehaviorActed<Self> becomes the user transition fold. Rust checks both method results against the generated complete Actions<Addr, Never, Sends, Birth> and declared error type.

The macro generates no runtime context, constructor, scheduler, mailbox, executor, interpreter, or template policy. Initialization and receive bodies remain application-authored pure folds.

Why one attribute

Rust attribute macros transform an attributed item and their output is unhygienic. The implementation therefore resolves absolute paths through the direct bombay-behavior dependency or the bombay-rs facade, including Cargo renames. See the Rust Reference on procedural macros.

Serde-style stacked helper attributes are formally supported for derive macros, which can register inert helpers. An impl-level attribute macro has no equivalent helper-registration mechanism. Keeping sends, births, and errors as named sections of the one owning #[behavior] invocation avoids expansion order as an API constraint and keeps one coherent generated fold.

This construction is Bombay policy, not an actor-model primitive. Actions remains Bombay's typed realization of communications, fresh actor creation, and the next behavior or termination.