Skip to content

feat(formula): one AST for both spreadsheet formula syntaxes - #884

Merged
andiwand merged 3 commits into
mainfrom
feat/formula-parser
Sep 12, 2026
Merged

feat(formula): one AST for both spreadsheet formula syntaxes#884
andiwand merged 3 commits into
mainfrom
feat/formula-parser

Conversation

@andiwand

@andiwand andiwand commented Sep 11, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Step 3.1 of docs/design/spreadsheet-editing.md — the read side of formulas. First of four; the dependency graph, the view and the .ods cached value follow on top of this.

What it does

src/odr/internal/formula parses what a table:formula and an <f> state into one AST. The two syntaxes share their expression grammar — the same operators at the same precedence — and differ over three things, so one recursive descent takes a Syntax and branches where they part:

OpenFormula OOXML
reference [$'My Sheet'.$A$1:.B2] 'My Sheet'!$A$1:B2
argument separator ; ,
array row separator | ;

Read: cell references, ranges, whole columns (A:A), the $ of each axis, sheet-qualified and external references, named expressions, functions, arrays, errors, and the operators. A named expression and a reference over several sheets (Sheet1:Sheet3!A1) stay the spelling the file states — nothing resolves one.

Nothing evaluates anything: that is step 4.

What it reuses

Two commits come first. TablePosition gains the non-throwing
try_to_column_num / try_to_row_num the parser needs — case folded, and no
longer wrapping past the index range — and the ascii character classes rtf, the
odf value cursor and this parser each wrote for themselves move into
util::string. Then internal::TextCursor takes the cursor layer both the odf
value cursor and this parser had written: peek, take, advance, seek,
skip_whitespace, consume, take_while.

Two decisions worth a look

  • A formula that does not parse answers nothing. A caller reads no reference out of one it cannot read, which keeps the dependency graph honest rather than half-built.
  • A spelling no position can hold is a name. A0 (rows are 1-based) and ABCDEFGHI1 (past what an index holds) are names, not refused parses — which is what a sheet calls them too, and a name carries no dependency.

Test

43 cases in test/src/internal/formula/formula_parser_test.cpp, inline strings only. They pin the precedence a sheet has (-2^2 is 4, -3% is -0.03), the LOG10( ambiguity against the reference LOG10, and each dialect's own spellings.

andiwand and others added 3 commits September 12, 2026 09:42
`TablePosition::try_to_column_num` and `try_to_row_num` answer an optional, so
a parser that must not throw on bad input can use them; the throwing pair is
written on top of them. Both fold case, because a spreadsheet reads a column
letter without it, and `to_column_num` no longer wraps silently on a spelling
past the index range.

The ascii character classes the parsers wrote each for themselves
(`is_ascii_digit`, `is_ascii_letter`, `is_ascii_letter_or_digit`, `to_upper`)
join `util::string`, and rtf, the odf value cursor and the odf geometry parsers
call them there. `is_ascii_space` is `is_ascii_whitespace`, which is what it
tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVmjmddv2Ui17Nptc1ggue
`internal::TextCursor` holds what the odf value cursor and the formula parser
each wrote for themselves: `peek`, `take`, `advance`, `seek`, `skip_whitespace`,
`consume` and the `take_while` over a character predicate. `odf::ValueCursor`
keeps only what an odf attribute adds — the comma as a separator, and the
number `std::strtod` reads out of a copied run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVmjmddv2Ui17Nptc1ggue
`internal/formula` parses what a `table:formula` and an `<f>` state into one
tree. The two syntaxes share their expression grammar, so one recursive
descent takes a `Syntax` and branches where they part: the reference
(`[Sheet1.A1:.B2]` against `Sheet1!A1:B2`), the argument separator and the
row separator of an array.

References, ranges, sheet-qualified references and named expressions are
read; nothing resolves a name or evaluates anything. A formula that does not
parse answers nothing, so a caller reads no reference out of one it cannot
read. A spelling past the grid is a name rather than a position, which is
also what `A0` and `ABCDEFGHI1` are.

Step 3.1 of `docs/design/spreadsheet-editing.md`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVmjmddv2Ui17Nptc1ggue
@andiwand
andiwand merged commit 83b4e1c into main Sep 12, 2026
25 checks passed
@andiwand
andiwand deleted the feat/formula-parser branch September 12, 2026 07:53
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