Reactive Execution
How Arc executes programs using reactive dataflow and stratified scheduling
Why This Matters
Consider two functions reading the same pressure sensor:
tank_pressure -> safety_check{}
tank_pressure -> controller{} In traditional programming, if tank_pressure updates to 500 psi while your code is
running, safety_check might see 500 psi while controller sees the old value (400
psi). This inconsistency could cause subtle bugs or safety issues.
Arc prevents this. Both functions see exactly the same value. Either both see 400 psi or both see 500 psi, never a mix.
This is called stratified execution, and it’s why Arc programs are predictable and safe.
The Reactive Model
Arc programs don’t execute line-by-line like traditional code. Instead, they use a reactive dataflow model where computations run automatically in response to new data arriving on channels.
You declare how data flows through your system, and Arc handles the scheduling:
// When tank_pressure updates, scale it and write to display
tank_pressure -> scale{factor=2.0} -> pressure_display When tank_pressure receives new data, Arc runs scale and writes the result to
pressure_display. You don’t poll or schedule anything yourself.
Flow Statements and Edges
Flow statements connect channels and functions using edges. There are two types:
Continuous Edges (->)
Run every time data arrives:
// Scale sensor readings continuously
tank_pressure -> scale{factor=2.0} -> pressure_scaled
// Run a control loop every 50ms
interval{period=50ms} -> controller{}
// Average two sensors
(sensor_1 + sensor_2) / 2.0 -> average_display Every time the source produces data, the entire pipeline executes.
One-Shot Edges (=>)
Fire once when a condition becomes true, then stop:
// Abort when pressure exceeds limit (fires once)
tank_pressure > 600 => abort
// Advance to next stage when target reached
tank_pressure > 500 => next
// Log a message once when pressure drops
tank_pressure < 20 => log_message{} One-shot edges reset when the containing stage is re-entered. Timing nodes (interval
and wait) also reset on stage re-entry. An interval fires immediately on the first
tick after re-entry, and a wait restarts its countdown from zero.
A common mistake is using -> when you meant =>. If you want something to happen
once (like a stage transition), use =>. If you use ->, the transition will keep
firing every cycle while the condition is true.
Stratified Execution
Arc guarantees deterministic, glitch-free execution through stratification:
Snapshot consistency: All nodes in an execution cycle see the same values. If a channel updates while the graph is executing, every node sees the same snapshot.
Deterministic order: Nodes execute in a guaranteed order based on their dependencies. The compiler organizes nodes into “strata” (layers), where each stratum only executes after all nodes in the previous stratum have completed.
Without this guarantee, debugging becomes difficult and safety certification nearly impossible. You’d never know if a bug was caused by inconsistent data or actual logic errors.
Stages and Functions
Arc programs have two kinds of code, and they serve different purposes.
Stage bodies wire your system together. They declare which channels feed which functions, and under what conditions the program should transition between stages. This is the reactive part of Arc. You don’t write loops or conditionals here. You describe data flow:
stage pressurize {
1 -> valve_cmd,
tank_pressure -> controller{} -> throttle,
tank_pressure > 500 => next
} Function bodies do the actual computation. They run once each time the reactive
system triggers them, and they support the same control flow you’d expect from any
programming language: variables, if/else, and
for loops:
func clamp(value f64) f64 {
if value > 500 {
return 500.0
}
if value < 0 {
return 0.0
}
return value
} Stages handle timing and ordering. Functions handle the math. This separation is what keeps Arc programs predictable.
Expressions in Flows
Inline expressions work as implicit functions:
// Comparison
temperature > 100 -> alarm{}
// Arithmetic
(sensor_1 + sensor_2) / 2.0 -> display
// Logical combination
pressure > 100 or emergency -> shutdown{} These expressions can reference channels and literals, but not local variables from function bodies.
Cycle Detection
Flow graphs must be acyclic. You can’t create a loop where node A depends on node B which depends on node A. The compiler will reject this with an error.
The exception is stage transitions using =>. These can form cycles because they
represent valid state machine behavior:
sequence main {
stage idle {
start_btn => next
}
stage running {
stop_btn => idle // valid: can return to idle
}
}