Formualizer Docs
Reference

Cycle Configuration Reference

The CycleConfig surface — detection modes, policies, iterative-calculation knobs, and how it maps across Rust, Python, and JS/WASM.

Cycle handling is configured through CycleConfig, nested under the engine's EvalConfig (and therefore reachable from WorkbookConfig.eval). It has two parts: a detection mode and a policy for live cycles.

CycleConfig {
  detection: CycleDetection,  // Static (default) | Runtime
  policy:    CyclePolicy,      // Error (default) | Iterate { max_iterations, max_change }
}

See the guides for the conceptual model:

CycleDetection

ValueBehavior
Static (default)Every static SCC is stamped #CIRC!. No live-edge machinery runs. Byte-for-byte compatible with prior releases.
RuntimeStatic SCCs are candidates; members evaluate with live-edge recording, and only live cycles get the policy verdict. Phantom (live-acyclic) SCCs produce ordinary values.

CyclePolicy

Applies to cycles confirmed live under Runtime detection.

ValueBehavior
Error (default)Live cycles produce #CIRC!.
Iterate { max_iterations, max_change }Excel-style iterative calculation. Live cycles run repeated passes until every member converges or the pass cap is reached. Requires Runtime detection.

Iterate knobs

KnobExcel defaultNotes
max_iterations100Total passes per cycle per recalc (pass 1 included). 1 = each member evaluates once per recalc (accumulator contract). 0 is a config error.
max_change0.001Absolute per-member convergence threshold (`

Retained fixed points

Every iterating SCC used to be redirtied at the end of each recalc, so circular cells re-evaluated on every recalculation. An SCC that landed on an exact fixed point is now left clean instead. This is not configurable: it changes only work done, never a value.

Exact fixed point means the recalc that iterated the SCC stopped because every member reproduced its previous value exactly — |Δ| == 0 for numbers (not merely |Δ| < max_change), identity for text, booleans and errors, never a NaN identity — before the max_iterations cap, and no member is volatile or holds a dynamic reference (INDIRECT, OFFSET). Such an SCC is a fixed point of its own inputs: re-running it with the same inputs cannot change any value.

A retained SCC re-runs when either of the following happens:

  • The dirty graph reaches a member — a precedent edit, a formula change, a structural edit, or a name rebinding. The dirty graph remains the validity authority; nothing about retention bypasses it.
  • The config fingerprint changes between recalcs. Knobs that can change a retained SCC's result without any edit touching the graph invalidate every retained SCC: cycle policy and tolerance, date system, workbook seed, volatile level, deterministic mode, and the range-expansion limits. Registering or replacing a function invalidates only the retained SCCs whose members call it.

These keep re-running on every recalc, unchanged:

  • Tolerance-only convergence — |Δ| < max_change but non-zero.
  • Capped SCCs — including the max_iterations: 1 accumulator contract.
  • Volatile or dynamic-reference cycles — anything containing NOW(), RAND(), INDIRECT, OFFSET, and friends.

See Retained fixed points for the telemetry and measured numbers.

Validation rules

A CycleConfig is rejected at build time (engine construction, with_cycle, or the binding setter) when:

  • Iterate is combined with detection: Static (iteration requires Runtime).
  • Iterate has max_iterations == 0.
  • Iterate has a negative or non-finite max_change.

Selecting the iterate policy through a binding promotes detection to Runtime automatically, so you rarely set both by hand.

Surface map

Equivalent configuration in each binding
use formualizer_eval::engine::{CycleConfig, CycleDetection, CyclePolicy};// Runtime detection only (live cycles still #CIRC!)let cfg = CycleConfig { detection: CycleDetection::Runtime, policy: CyclePolicy::Error };// Excel-default iterative calculation (Runtime + 100 / 0.001)let cfg = CycleConfig::iterate_excel_defaults();// Explicit knobslet cfg = CycleConfig::iterate(50, 0.0001);// Applied via EvalConfig::with_cycle (panics on invalid combos)let eval = formualizer_eval::engine::EvalConfig::default().with_cycle(cfg);

Binding parity notes

CapabilityRustPythonJS/WASM
Set detection mode✅✅ cycle_detection✅ cycleDetection (load only)
Set policy / iterate knobs✅✅ cycle_policy, iterate_*✅ cyclePolicy, iterate* (load only)
Configure on programmatic (empty) workbook✅✅ Workbook(config=...)❌ constructor takes no options
Read CycleTelemetry✅ engine().last_cycle_telemetry()✅ wb.last_cycle_telemetry()❌ config-only today
XLSX <calcPr> round-trip✅✅ (via load/save)✅ (via fromXlsxBytesWithOptions)

XLSX <calcPr> mapping

<calcPr> attributeCycleConfig
iterate="1"policy = Iterate, detection = Runtime
iterateCount="N"max_iterations = N (defaults to 100 if absent)
iterateDelta="D"max_change = D (defaults to 0.001 if absent)
iterate="0" / absent <calcPr>config left unchanged on load

On load, a file with iterate="1" wins over a configured Error policy (file-wins precedence). On save, an active Iterate policy writes the matching iterate/iterateCount/iterateDelta attributes.

On this page