ReferenceControlArcReferenceFlow

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.

Table

Operator Name Behavior
-> Continuous flow Fires whenever the source produces a value
=> Conditional flow Fires while the source is truthy

Continuous flow (->)

Runs whenever the source produces data:

sensor -> filter{} -> output

Continuous 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 => abort

A 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.

Table

Target Description
next Advance to the next item in the enclosing sequence
stage_name Jump to a named stage in the same sequence
sequence_name Jump to a different sequence (starts at its first 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:

  1. Deactivate current item in source sequence
  2. Activate first item of target sequence
  3. 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_out

Each 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.