Skip to content

Harden generated-model runtime and unblock 2024 FABLE conversion - #250

Merged
gparadis merged 2 commits into
mainfrom
feature/2024-fable-runtime-validation
Aug 15, 2026
Merged

Harden generated-model runtime and unblock 2024 FABLE conversion#250
gparadis merged 2 commits into
mainfrom
feature/2024-fable-runtime-validation

Conversation

@gparadis

Copy link
Copy Markdown
Contributor

Summary

Unblocks full-workbook conversion and scenario execution of the 2024 Canada FABLE Calculator workbook
(Zenodo 14755928). The pipeline (extract, graph, translate, infer-contract, generate) completes cleanly
(410,299 formulas translated, 0 untranslated, 0 output-closure blockers), and the generated model now
executes all 10,274 declared outputs without runtime errors.

Changes

  • Static INDIRECT support: resolve INDIRECT(ADDRESS(ROW()+-k, COLUMN()+-k)) to the target cell
    reference (used by 55 cells as a "value of the cell directly above" pattern), creating the proper
    dependency edge.
  • Corrupted structured-reference repair: drop the duplicated table prefix in
    name[] name[[#This Row],[Column]] during tokenization while preserving raw-formula provenance.
  • Excel-faithful runtime errors in generation.py: _sf_arith coerces numeric strings, returns
    #DIV/0!/#NUM!/#VALUE! instead of raising; ordering comparisons route through a new
    coercion-safe _sf_compare; VALUE/NUMBERVALUE return #VALUE!, LN returns #NUM!;
    _sf_sum/_sf_average propagate errors and ignore text cells.
  • INDEX over range views: _sf_index reconstructs rows from _SfRangeView instead of treating the
    range as a scalar.
  • Circular-dependency decision: the 1,741 static_circular_dependency warnings are phantom
    whole-column SUMIFS cycles (year-offset criteria exclude the self-cell); lazy sum-range evaluation
    means no runtime cycles. Kept as warnings and documented.

Validation

  • Scenario execution: 10,274 outputs, 0 errors (~100 s).
  • 300-output cached-value comparison: 94% match, mismatches 0.05-0.3% relative (Excel iterative-calc
    tolerance), no systematic defects.
  • Full test run: 217 passed, 1 skipped; ruff clean; git diff --check clean.

- Add static INDIRECT(ADDRESS(ROW()+-k, COLUMN()+-k)) resolution and
  repair the corrupted structured-reference prefix defect so the 2024
  Canada FABLE workbook converts end to end (410,299 formulas, 0
  untranslated, 0 output-closure blockers).
- Execute the generated model scenario (10,274 outputs) and fix all 349
  runtime output errors: Excel-faithful error strings from _sf_arith,
  _sf_compare, _sf_sum, _sf_average, _sf_ln, VALUE, and NUMBERVALUE;
  coercion-safe ordering comparisons; _SfRangeView resolution in
  _sf_index.
- Keep the 1,741 static circular-dependency warnings documented as
  phantom whole-column SUMIFS cycles that never form runtime cycles.
- Add regression tests covering the corrupted-ref repair, static
  INDIRECT, arithmetic/comparison coercion, SUM error propagation, and
  INDEX range views. Full test run: 217 passed, 1 skipped.
CI picked up ruff 0.16.3 via the unbounded ruff>=0.8 constraint; its
new lint rules flag 139 pre-existing violations repo-wide. Constrain
ruff to >=0.8,<0.16 so the quality job matches the version the codebase
passes cleanly against.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 530ba0ec67

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

" continue",
" total += value",
" count += 1",
" return total / count",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Return an Excel error when conditional averages select nothing

When an AVERAGEIF has no matching numeric cells, count remains zero and this division raises ZeroDivisionError, aborting the entire generated calculate() call unless the formula happens to be wrapped in IFERROR. Excel returns #DIV/0! in this case; _sf_averageifs repeats the same unconditional division and needs the same guard.

Useful? React with 👍 / 👎.

Comment on lines +646 to +648
" for value in (left_value, right_value):",
" if _sf_is_error(value):",
" return value",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate comparison errors through conditional expressions

When either operand is an Excel error, _sf_compare returns the nonempty error string, but generated IF formulas use comparisons directly as Python ternary conditions at generation.py:1264. Consequently, a formula such as =IF(#N/A<1,1,2) treats '#N/A' as truthy and returns 1 instead of propagating #N/A, silently changing workbook results.

Useful? React with 👍 / 👎.

Comment on lines +244 to +246
match = _STATIC_INDIRECT_ADDRESS_RE.search(raw_formula)
if match is None:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add every static INDIRECT target to the dependency graph

When one formula contains two supported INDIRECT(ADDRESS(...)) expressions, search() records only the first target in FormulaRecord.raw_references, although the parser later translates both references. Contract inference follows the graph, so the second target can be omitted from the generated constants/formulas and _get then supplies a blank, producing a silently incorrect result; collect all matches rather than returning only one.

AGENTS.md reference: AGENTS.md:L50-L53

Useful? React with 👍 / 👎.

Comment on lines +1024 to +1026
" if matched:",
" candidate = index + 1",
" break",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue scanning descending MATCH candidates

For MATCH(...,-1) on a descending array, the required result is the smallest value greater than or equal to the lookup value, but this branch returns after the first qualifying element. For example, lookup 7 in [10, 8, 5] returns position 1 instead of position 2; it should retain qualifying candidates until the first value below the lookup, as the match_type == 1 branch does in the opposite direction.

Useful? React with 👍 / 👎.

@gparadis
gparadis merged commit ec1c5d0 into main Aug 15, 2026
5 checks passed
@gparadis
gparadis deleted the feature/2024-fable-runtime-validation branch August 15, 2026 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant