Technical Article

Auditing Excel Formula Caches with HotXLS Deep Recalc

HotXLS answers the question every spreadsheet pipeline eventually has to ask, which is whether the numbers stored in a workbook still match the formulas that produced them. CalculateAndVerify recalculates the whole dependency graph into an isolated overlay, compares each result against the cached value already in the cell, and reports the disagreements. By default it changes nothing

The reason this matters is that a spreadsheet file stores two things per formula cell: the formula and the last value someone computed for it. Excel keeps them in sync. Everything else in the world may not. A file that passed through an older library, a partial recalculation, a hand-edited XML part or a tool that wrote values without recomputing them will happily present a total that no longer follows from its inputs, and nothing in the file format flags that

Why is a cached value that disagrees with its formula so dangerous?

Because it is invisible in every ordinary reading path. Open the file in a viewer, read the cell through an API, export it to CSV or PDF, and you get the cached number. The formula is right there in the same cell, and nobody compares them. The mismatch only surfaces when someone opens the workbook in Excel, which recalculates on load under most settings, and suddenly a report that was signed off last quarter shows different totals

The audit exists to make that comparison a deliberate, scheduled operation rather than an accident. It is the spreadsheet equivalent of verifying a checksum: cheap enough to run in an intake pipeline, and the only thing that turns a silent data-integrity problem into a report you can act on

var
  Book: TXLSWorkbook;
  Options: TXLSRecalcAuditOptions;
  Report: TXLSCalculationAuditReport;
  I: Integer;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('quarterly-close.xls');
    Options := TXLSRecalcAuditOptions.Default;
    Options.MaxIssues := 500;
    Report := Book.CalculateAndVerify(Options);
    try
      for I := 0 to Report.Count - 1 do
        if Report[I].Kind = xlcaiCacheMismatch then
          Writeln(Report[I].SheetName, '!',
                  Report[I].Row, ':', Report[I].Col, '  ',
                  Report[I].Formula,
                  '  cached=', VarToStr(Report[I].Actual),
                  '  recomputed=', VarToStr(Report[I].Expected));
      if Report.Truncated then
        Writeln('issue budget reached, raise MaxIssues');
    finally
      Report.Free;
    end;
  finally
    Book.Free;
  end;
end;

There are three overloads and they answer three different questions. The parameterless CalculateAndVerify returns a mismatch count, which is all a health check needs. The overload with an out array of mismatches gives you the cells. The overload taking TXLSRecalcAuditOptions returns a full TXLSCalculationAuditReport, which is the one to reach for when you need to know not just that a value disagrees but why the audit could not evaluate something

The overlay, and why the audit does not write

Every recomputed value lands in an overlay rather than in the cell cache, and the overlay is injected at the very front of the cell-read callback in both workbook engines. That placement is what makes the audit self-consistent: when B1 is recomputed and C1 depends on B1, C1 sees the value from this audit pass, not the stale cached one. Without that, a single upstream error would be reported once and then absorbed, and every downstream cell would appear to agree with a wrong input

Cells whose recomputed value matches the cache do not enter the overlay at all. That is not a micro-optimization, it is what keeps the audit affordable. A clean workbook with a hundred thousand formulas performs zero overlay writes and the pass stays inside a 1.35x budget against a full recalculation, which is the difference between something you can run on every intake and something you run once a quarter

HotXLS deep recalc audit pipeline: the workbook loads with caches untouched, every dependency node is marked dirty and evaluated once in topological order, recomputed values land in an isolated overlay consulted first by the cell-read callback in both engines, results are compared against cached values, classified through CalculateAndVerify into a TXLSCalculationAuditReport, and nothing is written to disk
Recomputed values land in an overlay ahead of the cell-read callback, matching cells never touch it, and the workbook on disk stays untouched unless ApplyResults commits a fully clean pass

Evaluation follows a serial topological order derived from the dependency graph, with every node marked dirty first, so each cell is computed exactly once after its inputs. If you want the incremental machinery that keeps a live workbook current instead of auditing a stored one, that is a different mechanism, described in incremental recalculation and the dependency graph

Failures are classified, not lumped together

A cell the audit cannot evaluate is not the same finding as a cell whose value disagrees, and TXLSCalculationAuditIssueKind keeps the categories apart. xlcaiCacheMismatch is the value disagreement. xlcaiMissingFunction and xlcaiMissingName say the evaluator met something it does not implement or cannot resolve. xlcaiUnsupportedArguments covers argument shapes outside the supported subset. xlcaiExternalReferenceDenied and xlcaiExternalReferenceMissing separate a policy refusal from an absent workbook. xlcaiCircularReference, xlcaiDataTableSkipped, xlcaiParseFailure, xlcaiCancelled and xlcaiInternalFailure complete the set

HotXLS audit issue classification: TXLSCalculationAuditIssueKind separates the value disagreement reported as xlcaiCacheMismatch from evaluation failure kinds such as xlcaiMissingFunction, xlcaiMissingName, xlcaiUnsupportedArguments, the xlcaiExternalReferenceDenied versus xlcaiExternalReferenceMissing pair, and xlcaiCircularReference, while a positive Excel error code counts as a result rather than a failure
One kind reports a value disagreement and the rest report why the evaluator could not judge a cell; an Excel error value is a computed result, so intentional error cells produce zero findings

One distinction is worth stating because it inverts a common assumption. A positive Excel error code is a result, not a failure. A cell that legitimately evaluates to #DIV/0! has computed correctly, so the audit stores that error in the overlay and compares it against the cache like any other value. A workbook full of intentional error cells produces zero findings, and a workbook where an error appeared or disappeared since the values were cached produces exactly the findings you want

Circular references get their own treatment. Nodes in a cycle never enter the topological order, so each is reported individually as xlcaiCircularReference, and the audit does not run the iterative solver. That is a deliberate read-only contract: whether iteration is enabled affects how the result code should be interpreted, not what the audit does. The mechanics of iterative evaluation are covered separately in iterative calculation and circular references

Reading a failure chain

When a formula fails to evaluate, knowing which cell failed is rarely enough, because the failure is usually three levels down a chain of references. Each issue therefore carries a Stack string rendered outermost frame first, in the form Sheet1!A1 > Sheet1!B2 > Data!C7, so the report points at the cell that actually broke rather than the cell you happened to look at

The recorder is bounded. MaxStackFrames defaults to 64 with a floor of 8, and the deepest failing chain is the one retained: an inner frame records the chain when the failure originates there, and outer frames unwinding afterward do not overwrite it. If any chain exceeded the budget, Report.StackTruncated is set, which tells you the difference between a short chain and a chain you did not see all of

HotXLS audit failure chain: when a formula three references down fails, the Stack renders outermost frame first, Sheet1!A1 then Sheet1!B2 then Data!C7, the innermost frame records the chain and outer frames unwinding do not overwrite it, MaxStackFrames defaults to 64 with a floor of 8, and Report.StackTruncated flags a chain you did not see all of
The Stack renders outermost frame first so the report points at the cell that actually broke, the deepest failing chain is the one retained, and StackTruncated separates short chains from truncated ones
// Read-only by default. ApplyResults commits the overlay only after a
// fully successful audit, under a write guard that rejects the commit
// if the workbook structure changed while the audit was running
Options := TXLSRecalcAuditOptions.Default;
Options.ApplyResults := True;
Options.AbsoluteTolerance := 0;     // exact comparison, surfaces drift
Options.RelativeTolerance := 0;
Options.OnProgress := HandleProgress;

Report := Book.CalculateAndVerify(Options);
try
  if Report.Applied then
    Book.SaveToFile('quarterly-close-repaired.xls')
  else
    Writeln('not applied: ', Report.Count, ' issues blocked the commit');
finally
  Report.Free;
end;

procedure THarness.HandleProgress(ASender: TObject;
  ACurrent, ATotal: Integer; var ACancel: Boolean);
begin
  ACancel := FUserRequestedStop;   // audit stops at the next node boundary
end;

When should you let the audit repair the workbook?

Only when the audit came back completely clean of failure-class issues, which is precisely the condition ApplyResults enforces for you. The commit happens after a fully successful pass, was not cancelled, and passes a structural guard: the binary engine watches a workbook change identifier, the OOXML engine snapshots a per-worksheet structure generation. If anything moved while the audit was running, the results describe a workbook that no longer exists and the commit is refused

Note the deliberate asymmetry. Cache mismatches do not block the application, because they are exactly what the commit is there to repair. Failure-class issues do block it, because a workbook where some formulas could not be evaluated would be half repaired, and a half-repaired workbook is worse than an unrepaired one that you know to distrust

Tolerance is a policy decision, not a default

The default comparison is a 1E-6 absolute tolerance with relative tolerance disabled, which preserves the classic behavior and quietly accepts a drift of 4E-7. That is usually right: floating-point evaluation order differences between whatever produced the file and the current evaluator will produce differences of that size on long sums, and reporting them as integrity findings is noise

Set both tolerances to zero when the question is different, when you are trying to find out whether an evaluator changed behavior between versions, or whether a third-party tool rewrites values in a subtly different way. At zero, the same 4E-7 drift becomes visible, and so does everything else. Choose the tolerance based on which question you are asking, and record the choice next to the report, because a report without its tolerance is not interpretable

Two neighboring capabilities complete the picture. When you want to know why a single formula produces the value it does, the step-by-step view in the formula evaluation tracer is the right tool. When you deliberately want cached values honored without any recomputation, for example on an intake path that must reproduce the file exactly as it arrived, that mode is described in reading cached formula values without recalculating. The audit is what sits between those two: it tells you whether trusting the cache is safe. It ships with the HotXLS Delphi spreadsheet component for both the binary and OOXML engines