Flow
Arc flow reference: continuous and conditional flow operators, transitions, expressions in flows, routing tables, and inline case bodies
Flow statements connect nodes in the reactive context. They are how data moves through an Arc program: from channels into functions, between functions, and out to destinations. Flow runs entirely in the reactive context and is distinct from statement-level control flow inside function bodies.
Continuous flow (->)
Runs whenever the source produces data:
sensor -> filter{} -> outputContinuous flows chain: each -> connects the previous node’s output to the next node’s
input. The chain executes whenever the leftmost source fires.
stage active {
true -> valve_cmd // writes true once on stage entry
sensor -> filter{} -> output // processes every sample
}If multiple -> flows write to the same channel in the same cycle, the last write by
line order wins.
Conditional flow (=>)
Fires on ticks where the source expression is truthy:
pressure > 500 => next
abort_btn => abortA conditional edge has no hidden state. On ticks where the source is truthy, it propagates to its target. On ticks where it is falsy, it does not.
Transition targets
A flow whose target is a stage or sequence name is a transition. Transitions
deactivate the current item and activate the target. They use => because they fire
while the channel value is truthy. A transition to a running target does not restart it,
except a transition to the item that holds it, which re-enters that item.
Using next
sequence test {
stage step1 {
condition => next // goes to step2
}
stage step2 {
condition => next // goes to step3
}
stage step3 {
// terminal stage
}
}Using next when there is no next item in the enclosing sequence is a compile-time
error.
Named stage transitions
sequence test {
stage idle {
start_btn => pressurize
}
stage pressurize {
pressure > 500 => hold
abort_btn => idle // can go backwards
}
stage hold {
time.wait{duration=10s} => complete
}
stage complete {
// terminal
}
}Cross-sequence transitions
sequence main {
stage pressurize {
emergency => abort // jumps to abort sequence
}
}
sequence abort {
stage safed {
false -> all_valves_cmd
}
}Cross-sequence transitions:
- Deactivate current item in source sequence
- Activate first item of target sequence
- No built-in “return” mechanism
Expressions in flows
Inline expressions act as implicit functions:
temperature > 100 -> alarm{}
(sensor_1 + sensor_2) / 2.0 -> display
pressure > 100 or emergency -> shutdown{}Expressions in flow position can reference channels and literals, but not local variables from function bodies.
Routing tables
A routing table is a brace-delimited map written on either end of a flow operator.
An input table gathers several sources into one function’s inputs. An output table
branches the flow: the output that fires decides which entry runs, instead of an
if/else.
// Input routing: many sources -> one function
{ sensor_1: a, sensor_2: b } -> average{}
// Output routing: the fired output picks the entry that runs
sensor -> demux{} -> { high: "high" -> log, low: "low" -> log }Custom routing
select is just a function
with two named outputs. Any function with multiple named outputs can drive a routing
table: declare the branches your logic needs as outputs, set the matching one in the
function body, and route each output by name.
func decide_stage{low f64, high f64} (value f64) (vent bool, press bool, abort bool) {
if value < low {
vent = true
} else if value <= high {
press = true
} else {
abort = true
}
}
tank_pressure -> decide_stage{low=10.0, high=80.0} -> {
vent: true -> open_vent, // write a channel
press: true => pressurize, // activate sequence: `pressurize`
abort: true => safe_state // activate sequence: `safe_state`
}Output names are arbitrary identifiers declared with the function’s named outputs, and the routing table keys must match them exactly.
Case bodies
The right side of an output routing entry, the case body, is a full flow statement
or an inline body. A bare target (true: next, false: shut_cmd) is an error.
flag -> select{} -> {
true: "on" -> log_str,
false: "off" -> log_str
}
sensor -> demux{} -> {
high: "above" -> log_str,
low: 1 -> log_num
}Inline stage body
An anonymous stage { ... } written directly as a case body. Its flows run in parallel
while the branch is active:
flag -> select{} -> {
true: stage {
"venting" -> log,
true -> vent_cmd,
pressure < 15 => shutdown
},
false: false -> vent_cmd
}Inline sequence body
An anonymous sequence { ... } written directly as a case body. Its items run in order:
sensor -> decide_stage{low=30.0, high=80.0} -> {
abort: sequence {
true -> cooler_cmd
time.wait{2s}
false -> cooler_cmd
}
}Mixed entries
A single routing table may freely mix all of the above:
sensor -> decide_stage{low=30.0, high=80.0} -> {
vent: true -> open_vent, // flow statement
press: "nominal" -> log, // flow statement
abort: stage { true -> cooler_cmd } // inline body
}Items inside an inline body are separated by newlines or commas; both are accepted.
Input routing
An input routing table precedes a function invocation and maps each source to a named input parameter:
func average(a f64, b f64) f64 {
return (a + b) / 2.0
}
{
sensor_1: a,
sensor_2: b
} -> average{} -> avg_outEach entry is source: parameter. The key on the left is a channel or expression; the
identifier on the right is the parameter it binds to. The function runs when any mapped
source produces data.
A function with more than one input requires an input routing table. Wiring a single source directly into a multi-input function is an error.
Output routing
An output routing table follows a function invocation and maps each named output to a destination:
func demux{threshold f64} (value f64) (high f64, low f64) {
if value > threshold {
high = value
} else {
low = value
}
}
sensor -> demux{threshold=50.0} -> {
high: true => high_out,
low: true => low_out
}Each entry is output: flow statement. The key is one of the function’s named outputs;
the body runs when the function sets that output. A reading of 75.0 runs the high
entry and leaves low untouched.
Inline bodies
Inline sequence and stage bodies are a shorthand for a branch that needs more than a chain. They behave like one-shot bodies activated by the routed output, and a few rules apply.
Anonymous only. An inline body cannot be named. true: stage foo { ... } is a
compile error; inline bodies cannot be referenced by other flows. Use a named top-level
stage or sequence if you need to reference the branch elsewhere.
No auto-advance. Firing and completing an inline body does not advance the enclosing sequence to its next item.
=> next is not valid inside an inline routing case body. Use a named top-level
stage or sequence if a branch needs to advance the enclosing sequence.
Cross-stage navigation works. An inline body can transition to a named stage, just like a normal transition:
flag -> select{} -> {
true: stage { time.wait{1s} => named_body }
}Arbitrary nesting. Inline routing tables may appear inside other inline bodies, to any depth.