Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Temporal state

Historical reads observe samples committed by earlier successful ticks. Current computation and historical visibility are separated by staging and one post-row temporal commit, but the storage owner depends on where the delay was bound.

Temporal state ownership and common commit Three panels distinguish delay storage. A direct top-level delay of an external environment variable reads monitor HistoryStore through HistoryAccess; its local delay commit is a no-op. A runtime-defined or internal delay owns an evaluator-local DelayState ring. A recursive self-delay also owns a local DelayState ring and stages the completed enclosing output. Positive delays return Deferred until their storage contains enough committed samples. All successful current-row samples become historical only at the common post-row commit. Historical reads have two storage owners and one visibility boundary During tick n, every positive delay reads state committed through n − 1; an unfilled read yields Value::Deferred. 1. Direct top-level external delay total = default(total[1], 0) + scaled HistoryStore(total) monitor-owned bounded history Delay via HistoryAccess read committed sample or Deferred local DelayState commit no-op for shared external history 2. Internal or runtime-defined delay dynamic or function body, internal graph NodeState::Delay evaluator-local DelayState ring Delay read ring; stage current operand pending operand from row n push only after row succeeds 3. Recursive self-delay previous enclosing stream output NodeState::Delay evaluator-local DelayState ring RecursiveDelay read previous output or Deferred pending stream output n stage after enclosing output exists successful post-row temporal commit HistoryStore records completed monitor values; local DelayState rings push staged samples. Both become readable at tick n + 1.

Reading rule. Each panel reads state committed through tick n-1. A direct top-level delay of an external environment variable reads the monitor's HistoryStore; internal, runtime-defined, and recursive delays read evaluator-local DelayState rings. Dashed paths stage or record successful row-n samples at the common commit, making them visible at tick n+1. An unfilled positive delay yields Value::Deferred.

Why staging is required

Evaluators run sequentially, but the language tick is synchronous. If a delay made its current operand historical as soon as its node ran, a later evaluator in the same row could observe a current value as though it belonged to the past. The common commit prevents that temporal aliasing.

A top-level direct delay of another environment variable—for example, prior_total = total[1]—is bound through HistoryAccess to monitor-owned HistoryStore storage. It reads the already committed history of that external variable; its node-local commit is a no-op because the monitor records completed values centrally.

A delay inside a runtime-defined body, function body, or other internal graph owns a NodeState::Delay(DelayState) ring in its containing EvaluatorState. It reads that ring during evaluation and stages its completed current operand. A RecursiveDelay uses the same local storage type but stages the enclosing stream's completed output.

A recursive delay inside an if branch belongs to the branch's evaluator state, but it still records the enclosing stream. For held = if x >= 0 then x else default(held[1], -1), the else branch reads the previous value of held, including a value the then branch produced; both branches are evaluated every tick, and the value the unselected branch computed never becomes history. The stream stages its output into every such delay once the whole if has produced it.

The running example

For total = default(total[1], 0) + scaled, total[1] is a RecursiveDelay because it reads the enclosing stream's own prior output. Tick 1 finds no committed local sample, so the delay produces Value::Deferred and default selects 0; total then produces 8. After the complete row succeeds, the recursive delay commits total = 8 to its local DelayState ring. Tick 2 reads 8, never the partially computed tick-2 value.

The bound graph makes the recursive representation and its post-output staging path explicit:

Bound evaluation graph with recursive fallback The total EvaluationGraph reads a RecursiveDelay and uses Const zero as Default's fallback, adds scaled from an EnvironmentSlot, and returns the Add node. The completed output is staged for the recursive delay and committed after the row. Bound EvaluationGraph for total RecursiveDelay { 1 } retained previous total Const(0) fallback before history exists Default node 0 or Const(0) External(slot 1) scaled from EnvironmentSlot Add node 2 output DataRef::Node post-output: stage RecursiveDelay, then commit after the completed row

Reading rule. Compilation reads this figure for how binding places operations; read it here for the dashed path only. The recursive delay is consumed during the forward pass, but the value it will return next tick is not written when its node runs — it is staged, and becomes historical at the commit after the whole row. That gap between the solid and dashed paths is the temporal boundary this page is about.

Delay state ownership

Monitor history is bounded per outer environment variable according to effective history requirements. It serves direct top-level external reads and can move destructively between compatible root monitors during context transfer.

Local DelayState belongs to the evaluator occurrence that implements the delay. Function call sites, recursive frames, and nested dynamic evaluators therefore have distinct temporal lifetimes even when they share immutable StreamProgram text. Positive offset determines local ring capacity.

New local state starts without samples unless compatible evaluator state is explicitly transferred, and returns Value::Deferred until enough activation-local samples have committed. Which replacements can donate that state is a property of the activation, not of history: see dynamic properties.

Retained environment and monitor history

Reconfigurable evaluation also carries a retained sparse environment used to lift outer values needed by nested expressions. That row may retain Value::Deferred or the last available outer value, but it is neither HistoryStore nor a DelayState ring and does not backfill newly activated temporal operators.

Which tier executes a delay

Everything above is canonical semantics, and it holds whichever tier runs the row. A delay is an ordinary scalar instruction, so a quickened region executes it over registers while its ring stays here, in the canonical arena. A native whole-schedule kernel can go further and commit the plan's temporal state itself, which makes it the one case where retention is owned outside this page's model until it is materialized back.

Neither changes when a value becomes historical: the commit is still the post-row barrier described above. Fusion and the scalar IR motivates the second representation, fusion and regions says which tier can run a fused temporal operation, and execution tiers covers the kernels and how their state comes back.

Failure and replacement

A failed tick performs no successful temporal commit and no monitor-history commit. The monitor then becomes terminal, so partially mutated non-temporal state is not reused by a later tick.

Root replacement moves local delay state only with semantically compatible mapped stream owners. Matching HistoryStore entries are destructively taken according to target requirements. Unmapped streams and histories start cold.

Implementation mapping

The ownership split is implemented by HistoryStore and HistoryAccess in monitor history code, NodeState::Delay(DelayState) in evaluator state, delay binding in the compiler, and the common commit path in DataflowMonitor and Evaluator.

Continue with language state for other persistent operators and context transfer for replacement behavior.