Skip to content

feat(document): answer which cells a formula reads - #886

Open
andiwand wants to merge 1 commit into
mainfrom
feat/sheet-dependencies
Open

feat(document): answer which cells a formula reads#886
andiwand wants to merge 1 commit into
mainfrom
feat/sheet-dependencies

Conversation

@andiwand

@andiwand andiwand commented Sep 11, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Step 3.2 of docs/design/spreadsheet-editing.md.

What it does

document.dependents(SheetPosition(0, 0, 0));   // who reads Sheet 1's A1?
document.dependents({a, b, c});                // one walk for a batch of edits
document.unresolved_formulas();                // who might read anything?

internal::SheetDependencies walks every sheet, parses every formula, and resolves each reference to the rectangle of a sheet it reads. A query then walks that reverse map transitively, so a formula reading a formula is named too.

Where it lives, and why

The graph is built the first time it is asked and kept on the document: writing a formula is refused until step 4, so nothing a write does changes it. It is reached through abstract::Document::sheet_dependencies() and held by internal::Document, which every engine derives from — so no engine writes the graph itself.

What each engine does write is one hook: SheetAdapter::sheet_visit_formulas, which hands out the cells the file spells rather than the positions they cover. That distinction is not cosmetic — a repeated ODS row of 1024 columns over 1048576 rows is a handful of nodes and a billion positions. odf walks the runs the parser indexed, ooxml its cell map, and an engine that drops the expression at parse time (.xls, .numbers) visits none.

The honest answer for what it cannot read

A formula that names something no position can be read out of — one that does not parse, a named expression (SUM(Sales)), a reference over several sheets (Sheet1:Sheet3!A1), a range whose corner is not a reference (A1:INDEX(B:B,3)) — is in unresolved_formulas() rather than silently reading nothing. It may read anything, and a caller that has to be right (#888, dropping a stale cached value in an .ods) treats those as dependent on everything.

A reference into another document is neither a dependent nor unresolved: no edit here reaches it.

Public surface

SheetPosition — the sheet by its place among the document's sheets, which is how an edit operation names one, and the cell by TablePosition. Ordered and hashable, like its neighbour.

Not bound yet (python/jni/apple/wasm). The host-facing shape of this is step 4's recalculate(operations) → changed cells; binding the graph query on its own can follow if it's wanted sooner.

Test

14 cases in test/src/sheet_dependencies_test.cpp, from inline .fods and .xlsx fixtures: the chain, ranges, whole columns, cross-sheet by name (without case), the batch, both unresolved kinds, and that every member of a shared group reads its own row.

@andiwand
andiwand force-pushed the feat/sheet-dependencies branch from de13e68 to 8f6aaa9 Compare September 11, 2026 22:15
@andiwand
andiwand force-pushed the feat/shared-formulas branch from 5abd74f to 0a3fbfc Compare September 12, 2026 06:50
@andiwand
andiwand force-pushed the feat/sheet-dependencies branch 2 times, most recently from 08dc7d0 to d9144fd Compare September 12, 2026 07:27
@andiwand
andiwand force-pushed the feat/shared-formulas branch 2 times, most recently from 412bd36 to 60a0943 Compare September 12, 2026 07:31
@andiwand
andiwand force-pushed the feat/sheet-dependencies branch 3 times, most recently from bb51ea6 to 79cedf7 Compare September 12, 2026 07:51
@andiwand
andiwand force-pushed the feat/shared-formulas branch 2 times, most recently from 5eebf32 to f2855af Compare September 12, 2026 08:03
Base automatically changed from feat/shared-formulas to main September 12, 2026 08:33
`internal::SheetDependencies` walks every sheet of a decoded document, parses
every formula it states and resolves each reference to the rectangle of a
sheet it reads. `Document::dependents(position)` then answers which cells read
that position, directly or through another formula, sorted and each named
once. A batch of positions costs one walk rather than one per position.

The graph is built the first time it is asked and kept on the document:
writing a formula is refused, so nothing a write does changes it. It is
reached through `abstract::Document`, and `internal::Document` holds it, so
no engine writes any of this - the walk goes through the element adapter
every engine already fills.

A formula that names something no position can be read out of - one that does
not parse, a named expression, a reference over several sheets, a range whose
corner is not a reference - is in `unresolved_formulas()` instead. It may read
anything, and the graph says so rather than claiming it reads nothing. A
reference into another document is neither: no edit here reaches it.

A position is the new `SheetPosition`: the sheet by its place among the
document's sheets, which is how an edit operation names one, and the cell by
`TablePosition`.

Step 3.2 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 force-pushed the feat/sheet-dependencies branch from 79cedf7 to be80cc7 Compare September 12, 2026 08:41
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