The Reactor Model#
The reactor model is a component-based programming paradigm designed for concurrent and distributed systems. It provides deterministic concurrency by structuring programs as compositions of isolated reactive components.
Reactors#
A reactor is the fundamental unit of composition. Each reactor encapsulates:
- Private state that cannot be accessed by other reactors
- Reactions that define behavior in response to events
- Ports for communication with other reactors
- Triggers (timers, actions) for generating events
struct Reactor {
Reactor** children; // Child reactors (hierarchy)
Reaction** reactions; // Reactions in this reactor
Trigger** triggers; // All triggers (timers, actions, ports)
Environment* env; // Execution environment
};
Reactors can be hierarchical—a reactor may contain child reactors, forming a tree structure. This enables modular design where complex systems are composed from simpler, reusable components.
Reactions#
A reaction is a procedure that executes atomically in response to triggering events. Reactions are the only way to modify a reactor's state or produce outputs.
struct Reaction {
Reactor* parent; // Owning reactor
void (*body)(Reaction*); // The reaction code
int level; // Topological level for ordering
Trigger** effects; // Triggers this reaction can affect
interval_t deadline; // Optional deadline constraint
};
Key properties of reactions:
- Atomic execution: A reaction runs to completion without interruption
- Triggered by events: Reactions only execute when their triggers fire
- Ordered deterministically: Reactions execute in tag order primarily and declaration order secondarily
- Not callable: Reactions cannot be called like functions—they only execute when triggered
Reaction Ordering#
When multiple reactions are triggered at the same tag, they execute in a well-defined order based on two rules:
- Topological ordering: Reactions are assigned levels based on the dependency graph. A reaction that reads from a port must execute after any reaction that writes to that port.
- Declaration ordering: Within the same reactor, reactions at the same level execute in the order they are declared in the source file.
Level 0: [Reaction A] [Reaction B]
│ │
▼ ▼
Level 1: [Reaction C] [Reaction D]
│
▼
Level 2: [Reaction E]
Triggers#
Triggers are event sources that cause reactions to execute. The uLF runtime supports several trigger types:
Timers#
Timers generate events at specified times, either once or periodically.
struct Timer {
Trigger super;
interval_t offset; // Initial delay
interval_t period; // Repeat interval (0 for one-shot)
};
A timer with offset=100ms and period=50ms fires at logical times 100ms, 150ms, 200ms, etc.
Actions#
Actions allow reactions to schedule future events. There are two types:
Logical Actions schedule events at a future logical time:
Physical Actions are triggered by external events (interrupts, callbacks) and bridge physical time into the logical time domain:
Startup and Shutdown#
Special built-in triggers fire exactly once:
- Startup: Fires at the beginning of execution (time 0, microstep 0)
- Shutdown: Fires when the program terminates
Ports and Connections#
Ports are the interfaces through which reactors communicate. Each port has a type and direction:
- Input ports receive data from upstream reactors
- Output ports send data to downstream reactors
struct Port {
Trigger super;
void* value_ptr; // Current value
size_t value_size; // Size of value type
Connection* conn_in; // Upstream connection (inputs)
Connection** conns_out; // Downstream connections (outputs)
};
Connections wire ports together, defining the communication topology:
Connection Types#
Logical Connections propagate values immediately (within the same logical time):
Delayed Connections introduce a time delay between sender and receiver. In Lingua Franca, these are specified with the after keyword:
struct DelayedConnection {
Connection super;
interval_t delay; // Time delay
// Value arrives at tag + delay
};
Physical Connections (using ~> in Lingua Franca) derive the logical time at the receiver from the physical clock rather than the sender's logical time. This is useful for modeling network communication with variable latency.
Multicast#
A single output port can connect to multiple input ports. When the output is set, all downstream inputs receive the value:
The is_present Pattern#
Ports and actions have an is_present flag that indicates whether they were set at the current logical time:
void my_reaction(Reaction* self) {
MyReactor* r = (MyReactor*)self->parent;
if (r->input_port.super.is_present) {
// Input was set at this tag
int value = *(int*)r->input_port.value_ptr;
// ... process value
}
}
This pattern enables reactions to handle optional inputs and distinguish between "no value" and "value is zero."
Hierarchy and Containment#
Reactors form a tree hierarchy. The main reactor is the root, containing all other reactors:
Child reactors are instantiated by their parent. Connections can cross hierarchy boundaries through port forwarding: