Introduction
Boomerang is a Rust runtime and composition framework for deterministic reactive systems. It is intended for robotics and embedded products whose software must remain understandable and testable as it grows across teams, cores, processes, and electronic control units (ECUs).
Applications are composed from typed reactors connected by actions and ports. Logical time and an analyzed dependency graph give those components a deterministic execution order. The long-term deployment model separates the application graph from its placement, allowing the same graph to run as one local system, as several local enclaves, or as a federation distributed across multiple targets.
This separation supports an iterative workflow:
- Develop and test reusable reactors independently.
- Compose and validate the complete logical graph.
- Run it locally with accelerated logical time in CI.
- Apply the production partitioning and exercise it in memory on one host.
- Deploy the same graph across the target cores and ECUs.
- Record physical or deployment boundaries and replay selected subsystems for regression and integration testing.
Assembly and Runtime Vocabulary
Boomerang separates declaring the logical application from executing it. Reactor
macros and manual APIs use a ReactorContext and ReactionDeclaration to record
typed specifications in an Assembly. Lowering resolves assembly keys and
deferred factories, producing a RuntimeAssembly whose enclaves can be executed
by the runtime or a federation runner:
flowchart LR
Declare["Declare<br/>contexts and declarations"]
Assemble["Assemble<br/>logical specifications"]
Lower["Lower<br/>resolve runtime objects"]
Execute["Execute<br/>runtime assembly"]
Declare --> Assemble --> Lower --> Execute
See the Glossary for definitions of these suffixes and the related keys, placement, partition, and runtime concepts.
Boomerang is an early-stage project. Deterministic logical-time execution,
local enclaves, modal reactors, recording/replay foundations, and experimental
static federation exist today. Deployment-independent partitioning,
boundary-layer replay, production multi-ECU deployment, mixed-criticality
policies, and no_std support are project goals rather than current guarantees.
See Project Goals and Status for the detailed distinction.
Origins
Boomerang is a Rust-first implementation of the Reactors deterministic actor model described by M. Lohstroh, A. Lee, and others at UC Berkeley in Reactors: A Deterministic Model for Composable Reactive Systems.
Lingua Franca is an important point of reference. Boomerang began as a Rust port of its discrete-event scheduler, but uses Rust types and macros for reactor behavior and composition instead of a separate coordination language. See also Reactor C++.
Project Goals and Status
Boomerang’s goal is to let teams describe deterministic application behavior once and preserve that behavior while changing how the system is tested and deployed.
Intended Users
Boomerang is aimed at engineers building robotics and embedded systems where:
- independently developed subsystems must compose through explicit interfaces;
- the same product may span several cores, processes, and ECUs;
- CI must exercise meaningful production behavior on a single host;
- recorded sensors or subsystems must be reusable in regression tests; and
- timing, ordering, and unsupported semantics must be explicit.
Reactors should be reusable without embedding assumptions about their eventual host, transport, or partition. Deployment configuration assigns a completed reactor graph to enclaves, federates, processes, and machines.
Design Goals
- Deterministic behavior: preserve logical tags, ordering, delays, mode transitions, and shutdown behavior across supported deployments.
- Late-bound deployment: separate graph composition from placement across threads, cores, processes, and ECUs.
- Component reuse: make typed reactors independently testable and hierarchically composable across products.
- Local-to-target continuity: run the same graph monolithically, with its production partitioning on one CI host, and on distributed targets.
- Recording and replay: capture nondeterminism at physical boundaries and substitute partitions using deployment-boundary recordings.
- Team-scale integration: use stable identities, explicit schemas, and early graph validation to support parallel development.
- Explicit failure semantics: reject unsupported distributed behavior rather than silently weakening determinism.
- Evidence-producing execution: compare stable logical traces across deployment and replay configurations.
Available Today
Boomerang currently provides:
- a deterministic logical-time scheduler;
- typed reactor composition in Rust;
- local enclave execution;
- modal reactors;
- recording and replay foundations; and
- experimental static in-memory and single-process TCP federation.
The current federation implementation is intentionally conservative. See Static Federation for its supported subset.
Long-Term Direction
The project intends to add deployment-independent graph partitioning, boundary-layer recording and substitution, multi-process and multi-ECU execution, and platform backends suitable for mixed-criticality embedded products.
The desired platform range includes std environments such as embedded Linux
and QNX, targets based on an RTOS such as Zephyr, and, where feasible, a
portable no_std runtime core. Host-side graph construction and validation do
not need to become no_std; constrained targets may instead execute a
statically lowered runtime plan with platform-provided clock, synchronization,
execution, storage, and transport services.
Mixed-criticality and safety-critical use are long-term goals. Boomerang does not currently claim hard real-time bounds, temporal or memory isolation, WCET or schedulability analysis, ASIL/SIL compliance, a qualified toolchain, or safety certification. Future claims must be backed by explicit scheduling and resource policies, fault-containment behavior, platform assumptions, tests, and reviewable evidence.
Quickstart
Modal Reactors
Modal reactors let one reactor contain several named modes where only one sibling mode is active at a logical instant. A mode is useful when a reactor has distinct phases, such as idle and active, and each phase owns different reactions, timers, actions, child reactors, or delayed connections.
The main user-visible effect is that work declared inside an inactive mode does not run. Mode-local logical time is suspended while a mode is inactive, so timers, logical actions, and delayed connections inside that mode do not age until the mode becomes active again.
Basic Syntax
Declare modes inside a #[reactor] function with mode! blocks. Exactly one sibling mode is marked initial.
use boomerang::prelude::*;
#[reactor]
fn Controller(
#[state] ticks: u32,
#[input] cmd: Command,
#[output] status: Status,
) -> impl Reactor {
mode! { initial idle {
reaction! {
(startup) {
state.ticks = 0;
}
}
reaction! {
(cmd) -> active, status {
if cmd.as_ref() == Some(&Command::Start) {
active.set(ctx);
}
*status = Some(Status::Idle);
}
}
} }
mode! { active {
let work = ctx.add_logical_action::<()>("work", Some(Duration::milliseconds(50)))?;
let tick = ctx.add_timer(
"tick",
TimerSpec::default().with_period(Duration::milliseconds(10)),
)?;
reaction! {
(tick) -> work {
state.ticks += 1;
ctx.schedule_action(&mut work, (), None);
}
}
reaction! {
(cmd) -> history(idle), status {
if cmd.as_ref() == Some(&Command::Pause) {
idle.set(ctx);
}
*status = Some(Status::Active);
}
}
} }
}
The names listed after -> in a reaction are effects. A mode effect is a typed transition handle, not a string. Calling active.set(ctx) requests the transition. Declaring -> active by itself does not change the mode.
Reset And History
The default transition kind is reset. These spellings are equivalent:
reaction! {
(cmd) -> active {
active.set(ctx);
}
}
reaction! {
(cmd) -> reset(active) {
active.set(ctx);
}
}
A reset transition enters the target mode with fresh local timing state. Pending mode-local logical actions, timers, and delayed connection deliveries in the target mode are discarded and restarted according to their declarations. Child reactors inside the reset mode return to their own initial modes. Rust state is not reset automatically; use a reset reaction when state must be restored.
A history transition preserves the target mode’s local timing state:
reaction! {
(cmd) -> history(active) {
active.set(ctx);
}
}
If a mode-local action had 2 ms remaining when the mode became inactive, it still has 2 ms remaining when the mode is re-entered by history. History also preserves logical microstep ordering for pending work at the activation tag, so multiple zero-delay local actions resume in the same order they would have had if the mode had stayed active.
Lifecycle Reactions
Modes can contain startup, reset, and shutdown reactions.
mode! { active {
reaction! {
(startup) {
state.entered_active = true;
}
}
reaction! {
(reset) {
state.reset_for_active();
}
}
reaction! {
(shutdown) {
state.active_was_seen = true;
}
}
} }
A mode-local (startup) reaction runs once, when that mode scope first becomes active. If the mode is initial, startup runs at program startup. If the mode is reached later by a transition, startup runs at the next microstep after the transition.
A (reset) reaction runs when its mode is entered by reset. Initial modes do not run reset reactions merely because the program started.
A mode-local (shutdown) reaction runs at program shutdown if its mode scope has been active at least once, even if it is inactive when shutdown happens. Shutdown reactions in unreachable modes do not run.
Local-Time Components
The following declarations are mode-local when written inside a mode! block:
- reactions;
- timers;
- logical actions;
- child reactors;
- delayed connections created by connecting ports with an
afterdelay.
Root-scope components, declared outside all modes, keep the usual global behavior and are always active.
Mode-local ports are not allowed. Ports are the stable interface of a reactor, so declare input and output ports at the reactor level and use them from mode-local reactions as needed.
Directly nested mode! blocks are not allowed. To model nested modal behavior, instantiate a child reactor inside a mode and give that child reactor its own modes.
Dependency cycles are checked against these static scopes. Reactions in sibling modes of the same reactor are mutually exclusive, and this remains true for child reactors declared inside those sibling modes. A dependency path that would be cyclic only by combining reactions from mutually exclusive modes is allowed because those reactions cannot run together at one logical instant.
Transition Timing
When a reaction requests a transition at tag (t, m), the current mode remains active for the rest of that tag. Work in the target mode can first run at a later tag. Immediate mode-local work, such as a reset reaction, startup reaction, or zero-offset timer, runs at (t, m + 1).
If multiple reactions request transitions for the same reactor at the same tag, deterministic reaction order decides the result and the last executed request wins. If one reaction sets two transition handles for the same reactor, the last .set(ctx) call in that reaction wins.
Physical Actions
Physical actions are accepted inside modes, but they are not suspended as local-time events. Physical actions are scheduled from wall-clock time. If the physical event is processed while its mode is active, its reactions can run. If the mode is inactive at that event tag, those reactions do not run, and history re-entry does not replay the physical event.
Use logical actions, timers, or delayed connections when a mode-local event should pause while inactive and resume on history entry.
Recording and Replay
Recording and replay make nondeterministic or unavailable parts of a system repeatable. Boomerang treats recording at two complementary boundaries.
Physical-Boundary Recording
Physical-boundary recording captures inputs from outside the deterministic reactor graph: sensors, clocks, operators, hardware interrupts, and external systems. Physical actions are the intended entry point for these inputs.
A recording preserves each value and its complete logical tag. Replaying those inputs into the same graph should reproduce the same observable logical trace, subject to the documented runtime and platform assumptions.
Deployment-Boundary Recording
Deployment-boundary recording captures messages crossing an enclave or federate interface. It allows CI to replace a partition—such as a sensor ECU or planning subsystem—with trace-backed endpoints while the rest of the graph runs live.
For a one-way producer, replay injects the recorded outbound messages at their original tags. For a bidirectional or feedback interface, a useful recording contains both directions: replay supplies the replaced partition’s outputs and validates that the live system produces inputs compatible with the recorded interaction. A static recording cannot respond correctly to novel inputs; that requires a behavioral model rather than replay.
Recording Contract
Portable recordings should use stable logical identities rather than runtime allocation keys. A boundary event needs at least:
- the stable endpoint or action identity;
- direction and payload schema;
- the full logical tag, including microstep; and
- deterministic ordering information for events sharing a tag.
Recordings may also carry a graph or interface fingerprint so incompatible graphs fail clearly instead of producing misleading results.
Current Status
Boomerang currently has action recording/replay foundations backed by MCAP. Stable deployment-independent identities, full boundary recording, partition substitution, and deployment-equivalence trace comparison are architectural goals and are not yet complete. In particular, deterministic replay must preserve microsteps and must not depend on enclave or action keys that can change when a graph is repartitioned.
Static Federation
Boomerang has an experimental federated feature for static federated
reactors. A federate is a reactor instance placed behind
add_child_federate; cross-federate logical messages are serialized with a
registered payload codec and coordinated by a runtime infrastructure loop
(RTI).
The in-memory and TCP runners execute persistent static federates with the same logical-time scheduler hooks used by the protocol client. A typical setup registers a codec, builds runtime parts, and then selects a runner:
let mut assembly = Assembly::new();
assembly.register_federated_codec::<u32, _>(boomerang::federated::SerdeJsonCodec)?;
let config = runtime::Config::default().with_fast_forward(true);
let parts = assembly.into_runtime_assembly(&config)?;
let envs = execute_federation_in_memory(parts, config)?;
Static federation currently requires fast-forward execution because a common
physical start is not implemented. Omitting .with_fast_forward(true) returns
an unsupported-configuration error instead of running schedulers against
independently initialized wall clocks.
The TCP runner is also synchronous and single-process. It starts a static RTI listener, connects every federate scheduler through the shared TCP protocol transport, and returns the same final runtime environments:
let config = runtime::Config::default().with_fast_forward(true);
let parts = assembly.into_runtime_assembly(&config)?;
let envs = execute_federation_over_tcp(
parts,
config,
TcpStaticFederationConfig::default(),
)?;
The default TCP configuration binds 127.0.0.1:0, so the operating system
selects an unused localhost port. This runner proves real framed transport; it
does not launch separate processes or provide dynamic federation membership.
Socket arrival order does not establish identity: each accepted peer declares
its preconfigured federate id in Hello, while membership remains static.
Payload encoding, transport, RTI protocol, and outbound delivery failures are returned to the runner’s caller. They are not treated as permission to process a logical tag.
The supported subset is deliberately conservative. It supports static persistent federates, one runtime enclave per federate, logical cross-federate messages routed through the RTI, same-tag messages, same-timestamp microsteps, fanout, multi-hop topologies, shutdown/no-future coordination, and positive-delay distributed cycles.
The implementation rejects cross-federate physical connections, transient
federates, mixed local/federated boundaries, and distributed zero-delay cycles.
It does not implement PTAG or ABS, dynamic federate join/leave, reconnect
behavior, authentication, or direct federate-to-federate payload channels.
Run the public in-memory federation proof with:
cargo test -p boomerang --features federated public_api_runs_static_in_memory_federation
Run the ignored localhost TCP proof with:
cargo test -p boomerang --features federated tcp_static -- --ignored
If a sandbox reports Operation not permitted while binding localhost, rerun
that focused command with socket permission. The failure is environmental; the
non-network in-memory tests remain the primary correctness suite.
Glossary
This glossary defines the main terms used by Boomerang’s public API and
documentation. In type names, suffixes such as Spec, Context, and Factory
describe where a value belongs in the lifecycle from application declaration to
runtime execution.
Action
A typed event owned by a reactor. A logical action is scheduled in logical time;
a physical action introduces an event from outside the deterministic reactor
graph. TypedActionKey<T, Q> identifies an action while retaining its payload
and action-kind types.
Assembly
The complete build-time model of a Boomerang application. Assembly stores the
declared reactor, reaction, action, port, mode, and connection specifications,
validates their relationships, analyzes dependencies and partitions, and then
lowers the model into a RuntimeAssembly.
The published crate remains named boomerang_builder, and the top-level facade
continues to expose it as boomerang::builder. Within that package, however,
Assembly is the name for the build-time graph rather than “environment” or a
generic builder.
Assembly Error
AssemblyError reports failures while declaring, validating, or lowering an
assembly. Examples include duplicate definitions, invalid connections,
dependency cycles, unsupported federation topology, and unresolved assembly
keys.
Assembly Fully Qualified Name
AssemblyFqn is a hierarchical name for an object in an assembly, such as a
reactor, action, reaction, or port. It is useful for lookup and diagnostics.
An assembly FQN describes the declared graph; it is not yet the durable logical
identity promised by the future deployment-independent recording model.
Assembly Key
A slot-map identity for one declaration stored in an Assembly. The concrete
types are AssemblyReactorKey, AssemblyReactionKey, AssemblyActionKey,
AssemblyPortKey, and AssemblyModeKey.
Assembly keys are valid while constructing and lowering that assembly. They are
not stable recording identifiers and must not be persisted as
deployment-independent identity. Typed wrappers such as TypedActionKey,
TypedPortKey, and TimerActionKey add domain and payload information around
the corresponding assembly key.
Boundary
A connection point between runtime partitions. A boundary may be local between
enclaves or federated between coordinated participants. BoundaryKind and
InterPartitionPlan describe the result of boundary analysis during lowering;
the runtime backend supplies the corresponding delivery mechanism.
Connection
A declared route from an output port to an input port. A connection may have a logical delay and may remain within an enclave or cross an enclave or federate boundary. Lowering chooses the runtime delivery mechanism while preserving the connection’s logical behavior.
Context
A temporary cursor used while declaring part of an assembly. ReactorContext
is the primary example: reactor macro output and manual declaration code use it
to add ports, actions, modes, child reactors, reactions, and connections. A
context mutates the assembly; it is not a stored graph node and does not survive
lowering. Local variables conventionally use the short name ctx.
Declaration
A fluent, temporary API that records a specification in an assembly.
ReactionDeclaration collects a reaction’s triggers, uses, effects, mode scope,
and function before finish records a ReactionSpec. This is distinct from the
stored specification and from a factory that creates a runtime object later.
Enclave
A runtime scheduling partition. Reactors within one enclave share a scheduler and can use direct runtime relationships. Connections between enclaves require asynchronous boundary delivery. An enclave is a runtime execution boundary; it is not synonymous with a federate, process, or host.
Factory
A callable value that creates a runtime object once lowering has resolved the
runtime keys and aliases it needs. The Factory suffix distinguishes deferred
runtime creation from assembly declaration. Examples include
ActionFactoryFn, DeferredReactionFactory, and the DeferredRuntimeFactory
trait.
Factories may capture declaration-time configuration, but they run at the assembly-to-runtime boundary rather than adding new specifications to the assembly.
Federate
A statically identified participant in coordinated federated execution. In the current experimental federation slice, each federate is placed at an enclave root and communicates through the runtime infrastructure loop (RTI). Federates, enclaves, processes, and hosts are separate concepts even where the current runner maps them one-to-one. See Static Federation.
History Transition
A mode transition that preserves the target mode’s local timing state. Pending mode-local logical actions, timers, and delayed connections resume with the same remaining local delay they had when the mode became inactive.
Local Time
Logical time measured only while a mode scope is active. Mode-local timers, logical actions, and delayed connections use local time.
Logical Time
The deterministic time coordinate used to order Boomerang events independently
of wall-clock scheduling. A Tag contains a timestamp offset and a microstep;
microsteps distinguish ordered events at the same timestamp. Logical actions,
timers, delayed connections, federation messages, and recordings must preserve
the complete tag.
Lowering
The consuming pass that transforms an Assembly into executable runtime data.
Assembly::into_runtime_assembly validates the graph, chooses partitions,
allocates runtime reactors, actions, ports, modes, and reactions, resolves
assembly keys to runtime keys, and produces a RuntimeAssembly.
Lowering is distinct from declaration: declaration records the logical graph; lowering materializes a particular runtime representation of it.
Mode
A named state of a reactor. Exactly one sibling mode is active at a logical
instant. ModeSpec stores a declared mode, while ModeEffectSpec describes a
reset or history transition requested as a reaction effect.
Mode Scope
The static region of a reactor contained by a mode. Reactions, timers, logical actions, child reactors, and delayed connections declared inside the mode belong to that scope.
Port
A typed data endpoint on a reactor. Reactions read input ports and write output
ports; connections route values between compatible ports.
TypedPortKey<T, Q, A> retains the payload type, direction, and locality
information used during safe declaration.
Partition
A region of the declared graph selected to execute behind one runtime boundary.
Current lowering maps each partition root to an enclave and records
cross-partition connections in an InterPartitionPlan. A partition is a graph
and deployment concept; it is not automatically a process, host, or federate.
Reaction
A deterministic unit of behavior that runs when one of its triggers is present.
A reaction may read declared uses and write declared effects. Its declaration is
collected through ReactionDeclaration, stored as ReactionSpec, and lowered
to a runtime reaction function.
Reactor
The main compositional component in Boomerang. A reactor owns state, ports,
actions, reactions, modes, and child reactors. The Reactor trait is the
application-facing construction interface commonly implemented by generated
macro code; ReactorSpec is the type-erased declaration stored in an assembly.
Reactor Placement
ReactorPlacement records whether a declared reactor remains local, begins a
new enclave, or represents a federate. Placement influences partition analysis
during lowering without changing the reactor’s logical behavior.
Reset Transition
The default mode transition. It enters the target mode with fresh local timing state, discards pending mode-local events in the reset scope, and returns contained modal child reactors to their initial modes.
Runtime Aliases
RuntimeAliases maps assembly keys to the runtime keys allocated during
lowering. Deferred factories use these maps when they need the runtime identity
of an object declared earlier. These aliases are runtime-construction data, not
durable application identity.
Runtime Assembly
RuntimeAssembly is the ready-to-run result of lowering an Assembly. It owns
runtime enclaves and the resolved metadata needed by features such as replay and
static federation. Runners consume it to create schedulers or federation roles;
it no longer accepts logical graph declarations.
Runtime Environment
boomerang_runtime::Env is the executable state owned by a runtime enclave,
including runtime reactors, actions, ports, reactions, and dependency data. It
is genuine runtime “environment” vocabulary and is distinct from the build-time
Assembly. Runners may return final runtime environments for inspection after
execution.
Specification (Spec)
A stored build-time declaration inside an Assembly. ReactorSpec,
ReactionSpec, ActionSpec, PortSpec, ModeSpec, and ConnectionSpec
describe the logical graph before runtime allocation. TimerSpec,
ModeEffectSpec, and FederateSpec are focused configuration specifications.
A Spec is data that the assembly validates and lowers. It is not the temporary
context or declaration API used to record that data, and it is not the runtime
object produced afterward. Traits such as ErasedPortSpec,
ErasedConnectionSpec, and ParentReactorSpec provide type-erased or shared
views over stored specifications.
Tag
The concrete logical-time coordinate of an event: a timestamp offset plus a microstep. Tags provide deterministic ordering, including multiple causally ordered events at the same timestamp. Recording, replay, and federation must preserve both components.
Timer
A built-in logical action scheduled from a TimerSpec. A timer may have an
initial offset and an optional period. TimerActionKey is the typed declaration
key used to trigger reactions from that timer.