CegWorkbook — a thin wrapper that suppresses phantom #CIRC errors from mutually-exclusive IF branches #99
Replies: 4 comments
-
|
Thanks for writing this up, this is a really useful breakdown of the failure mode. I think the core diagnosis is right: One thing that would help a lot before we choose the exact shape: do you have a sanitized workbook, or even just a few representative real formulas, where this comes up? I'm especially curious whether the guards are mostly simple flags like: or more like: That difference matters quite a bit. A bare flag-cell case is much easier to reason about than arbitrary predicates, and I would rather not design around the toy example if the real workbooks are doing something broader. My current instinct is that there are two related but separate things here. First, there is a nearer-term Formualizer-native fix around control-flow-aware dependencies. The places for that are probably Then the scheduler does not need to solve the whole boolean satisfiability problem up front. It can treat unconditional cycles as hard That seems preferable to making the planner prove "this cycle is impossible" statically. It also handles cases where the guard is not just a bare cell reference, or where the guard changes between recalcs. We already have the Second, there is the bigger Excel-parity topic of iterative calculation. Excel's normal answer for circular models is "enable iterative calculation" with max-iterations and tolerance. Formualizer doesn't have that yet. That would cover real fixed-point cycles too, not just phantom branch cycles. Finance models with circular interest, debt sweeps, depreciation/tax feedback loops, etc. tend to live in that world. But that is a larger feature and probably deserves its own design issue because it raises questions about convergence policy, previous-iteration values, arrays/spills, volatiles, and so on. So I think we should open two issues:
The first one is likely the right starting point for this specific Very open to contributors here. We'd just want to keep the work aligned with that split so we don't end up with a parallel wrapper/interpreter path that is hard to keep consistent with the engine. The CEG prototype is still useful though, especially as a concrete explanation of the branch-guarded-cycle shape and as a source of test cases. |
Beta Was this translation helpful? Give feedback.
-
Real Case: "Routing Switch" Dependency Cycles in our Engineering ModelsTo anchor this in actual production behavior, this problem heavily impacts our engineering design workflows. We maintain a highly adaptable spreadsheet designed to model various simulation scenarios. Because VBA was not an option for our environment, we rely on "guard cells" to dynamically steer the calculation logic. |
Beta Was this translation helpful? Give feedback.
-
|
My initial thought for a fix was actually something akin to "branching" —where the engine splits into separate routes on each condition, evaluates them independently, and then merges them back into the main flow once the remaining steps align. |
Beta Was this translation helpful? Give feedback.
-
|
sorry for the late response but i will give it more time on the weekend :) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
CegWorkbook— a thin wrapper that suppresses phantom#CIRCerrors from mutually-exclusiveIFbranches@VTO-AhmedHassan @ahah43 @DrRoot85
Abstract
formualizer correctly raises
#CIRCwhen two cells reference each other — but not all such cycles are real. When the cross-references sit inside mutually-exclusiveIFbranches, the cycle exists in the static dependency graph yet can never execute at runtime. This project introduces a Conditional Edge Graph (CEG) layer that annotates each dependency edge with the condition under which it fires (WhenTrue(guard),WhenFalse(guard), orAlways), detects cycles whose edges are provably contradictory, and evaluates those cells lazily using short-circuitIFsemantics — suppressing the phantom#CIRCwithout touching formualizer internals. Phantom detection uses backtracking DFS over the SCC sub-graph to test the satisfiability of every simple cycle's condition conjunction, handling chains of any length. Real cycles are preserved. The implementation wrapsWorkbookviaDeref, so the calling code changes by exactly one word.The problem
Consider this valid spreadsheet pattern:
When
A1 = TRUE, A2 takes its true branch (555) and A3 follows A2. WhenA1 = FALSE, A3 takes its false branch (999) and A2 follows A3. At runtime, only one direction of the A2↔A3 reference is ever active — the other is dead code for that value of A1. The spreadsheet is mathematically sound.formualizer's cycle detector, however, sees the flat dependency graph: A2→A3 and A3→A2. It raises
#CIRCfor both cells regardless of A1's value.The idea: Conditional Edge Graph (CEG)
Instead of a flat graph, annotate each dependency edge with the condition under which it is active:
WhenFalse(A1)— only active when A1 is falseWhenTrue(A1)— only active when A1 is trueA cycle made entirely of edges whose conditions contradict each other (
WhenTruevsWhenFalseon the same guard cell) can never execute at runtime. It is a phantom cycle — real in the static graph, impossible at runtime.The CEG layer:
formualizer-parse) and walks it, annotating edges insideIFbranches asWhenTrue(guard)/WhenFalse(guard)and everything else asAlwaysevaluate_cellon them (so formualizer never sees the phantom dependency)Usage — nearly identical to
WorkbookCegWorkbookimplementsDeref<Target = Workbook>andDerefMut, so everyWorkbookmethod is available directly. The CEG-aware methods (set_formula,evaluate_cell) shadow the inner versions automatically.Wrapping an existing workbook loaded from XLSX is one line:
Demo output
Running three cases side by side — phantom cycle, real cycle, and a normal chain — produces:
Real cycles are preserved correctly (Case 2). Normal cells are unaffected (Case 3). Only phantom cycles are suppressed (Case 1).
Current scope and known limits
IF(cell, …)andIF(AND(cell1, cell2), …)— guards that reduce to concrete cell referencesIF(VLOOKUP(…)>0, …)produceAlwaysedges (no suppression, no false positives)Open questions for the authors
Would something like this be worth building natively into formualizer's cycle detector — perhaps as an opt-in pass that annotates edges during
set_formula? Or is the current design intentional (strict cycle detection, leave suppression to the caller)? Happy to discuss the approach or refine the implementation based on your feedback.The Implementation Code
Beta Was this translation helpful? Give feedback.
All reactions