HF-131 (5/7): no error reports a cell that did not produce it - #1765
marcin-kordas-hoc wants to merge 11 commits into
Conversation
An error read out of a neighbouring cell is not produced by the cell that read it. This closes the remaining rootless-origin cases the choke-point redesign's propagated-skip needed but did not itself provide identities for: - A parsing error is attributed to 'parser' (ParsingErrorVertex.getCellValue). - An error value typed directly into a cell is attributed to 'user input' (CellContentParser's CellContent.Error constructor). - An error literal written into a formula (e.g. =#VALUE!+1) is attributed to 'literal' (FormulaParser's ErrorLiteral alternative). - A #SPILL! error reports the cell that failed to spill, not no address at all: setNoSpace() attaches itself as root, since it already has 'this' in hand. getScalarValue's range-to-scalar coercion class was measured, not assumed: the requesting cell is genuinely the one that made the invalid request, so 'no address' there is honest, not a bug, and is left unchanged. known-limitations.md gains one bullet: the six buildErrorWithRawInputAst sites in FormulaParser.ts (malformed references) stay unattributed rather than mislabeled, pending a future task. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Task linked: HF-131 Verbose and actionable formula error messages |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | e2ced02 | Commit Preview URL Branch Preview URL |
Sep 15 2026, 08:03 AM |
Performance comparison of head (e2ced02) vs base (624a8e4) |
|
Ran a closed-loop comparison of this stack's origin/address behavior against real Excel via One thing surfaced that isn't a bug, but is worth a product call: the CRUD-produced Should a CRUD-removed reference get an |
…rigin # Conflicts: # CHANGELOG.md
An error that describes a broken reference exists before any function sees it, so the enclosing call is only a reader. originNameForAstNode returns undefined for reference and error nodes, so such an error reached the enclosing FUNCTION_CALL unclaimed and non-propagated, and the postprocessing tail handed it to that call: =SUM(A2:A99999999999) reported SUM, =ABS(A2:A99999999999) reported ABS with an argument index that belonged to nothing, and =SUM(#REF!) after removing rows reported SUM. The identity is now attached where the error is built, which lets the existing first-wins rule block the reader by itself. A reference that cannot be resolved reports 'reference', whether the parser rejected it, the sheet is missing, the range spans several sheets, or the name is unknown; one destroyed by removing rows or columns reports 'removed reference'; the out-of-sheet error raised inside the OFFSET heuristic reports 'OFFSET', because OFFSET really did produce it. Errors that arise inside an array result are unchanged and still take the reading function's name; that needs either a hook over every ad-hoc array element or the function name threaded through three methods of the plugin base class, so it is left out and stated in the known limitations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…that made it An error that is one element of a returned array never reached the scalar branch of evaluateAst's tail, so it arrived at the enclosing call unclaimed and that call took it: =INDEX(A1:A2+B1:B3, 3, 1) reported INDEX for an #N/A the + operator produced, and a per-element coercion failure carried an argumentIndex with no originFunction at all, which the public JSDoc promises never happens. The tail now stamps the elements of an array the node itself built, which is the same first-wins rule applied one level down. A range-backed value is skipped: it is a view over cells whose errors were already marked when they were read, and reading its data would materialise the whole range on the hot path. That guard is not cosmetic — without it 'Sheet A' measured +5.7% against a 0.6% base spread; with it, four alternating rounds against the branch point give -1.09% overall and nothing outside its own spread. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2a81d2f. Configure here.
Bugbot caught a site the previous commit missed, in the same family. An array literal
whose rows do not line up builds a #REF! before any call sees it, and originNameForAstNode
had no case for that node, so the enclosing call took it: =SUM({1,2;3}) reported SUM and
=ABS({1,2;3}) reported ABS with an argument index that belonged to nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/hf-131-origin-function #1765 +/- ##
===============================================================
+ Coverage 97.40% 97.43% +0.02%
===============================================================
Files 195 195
Lines 15846 15862 +16
Branches 3494 3500 +6
===============================================================
+ Hits 15435 15455 +20
+ Misses 403 399 -4
Partials 8 8
🚀 New features to boost your workflow:
|

What and why
Stacked on #1764. Closes the remaining cases where an error could report a cell that did not
produce it — the general case (propagation through a function/operator) is already covered by
the mechanism in #1762; this handles the rest:
originFunction: 'parser'(ParsingErrorVertex.getCellValue).#REF!, no leading=) reportsoriginFunction: 'user input'.=#REF!) reportsoriginFunction: 'literal'.#SPILL!errors now report the address of the cell that failed to spill, instead of no addressat all —
setNoSpace()now attaches the vertex as root, the same way every other formula resultdoes.
Known, documented limit: the six reference-checking branches in the formula parser that build
a
#REF!/#NAME?error for a malformed reference (e.g. a range spanning more than one sheet)still report no
originFunction— left unattributed rather than mislabeled as a function that onlyread the value. Noted in
docs/guide/known-limitations.md.Verified
npx tsc --noEmit— cleannpx eslint src/— 0 errorspre-existing branch-pinning mismatch as the rest of this stack
reading another cell's error reports that cell's address, not its own reading cell's, through
chains up to 3 hops and across sheets; a parsing error's address is preserved through a reader;
a
#SPILL!fixture now reports the failing cell's address where it previously reported noneStack
5 of 7 — stacked on #1764. Next:
fix/hf-131-issue-444-addresses.🤖 Generated with Claude Code
Note
Medium Risk
Touches core evaluation, parsing, and dependency-graph error paths; behavior of
DetailedCellErrormetadata changes but formula results should stay the same.Overview
HF-131 (5/7) tightens error origin and address attribution so consumers of
DetailedCellErrorsee who produced an error and which cell it belongs to.Origins at the source: Parsing failures use
parser; literal#REF!typed in a cell usesuser input;=#REF!in a formula usesliteral. Broken or missing references (parser, interpreter, dependency graph) usereference; references invalidated by row/column deletes useremoved reference;OFFSETout-of-bounds usesOFFSET. That stops enclosing functions from claiming errors they only read (e.g.SUMon an oversized range).#SPILL!:setNoSpace()now attaches the formula vertex as root so the spill error reports the cell that could not spill, not a missing address.Arrays: After evaluation, errors inside ad-hoc array results get
stampOriginForArrayElements, so broadcast gaps and per-element failures credit the operator that built the array (e.g.+inINDEX(A1:A2+B1:B3, …)) instead of the function that indexes the array.CHANGELOG and known-limitations document the origin vocabulary and that
#SPILL!/#CYCLE!have nooriginFunction.Reviewed by Cursor Bugbot for commit e2ced02. Bugbot is set up for automated code reviews on this repo. Configure here.
Follow-up in this PR: a function no longer claims an error it only read
An error describing a broken reference exists before any function sees it, so the enclosing call is
only a reader.
originNameForAstNodereturnsundefinedfor reference and error nodes, so such anerror reached the enclosing
FUNCTION_CALLunclaimed and non-propagated and the postprocessing tailhanded it over:
=SUM(A2:A99999999999)reportedSUM,=ABS(A2:A99999999999)reportedABSwithan argument index that belonged to nothing, and
=SUM(#REF!)after removing rows reportedSUM.The identity is now attached where the error is built, so the existing first-wins rule blocks the
reader by itself. A reference that cannot be resolved reports
reference— whether the parserrejected it, the sheet is missing, the range spans several sheets, or the name is unknown — and one
destroyed by removing rows or columns reports
removed reference. The out-of-sheet error raisedinside the
OFFSETheuristic reportsOFFSET, becauseOFFSETreally did produce it.Open question for review:
removed referenceis a new public vocabulary word, and it is thenaming question raised earlier on this PR. It follows the existing lowercase-prose identities
(
parser,user input,literal).Errors that arise inside an array result are unchanged and still take the reading function's name —
=INDEX(A1:A2+B1:B3, 3, 1)reportsINDEXfor an#N/Athe+operator produced. Covering themneeds either a hook over every element of every ad-hoc array, which changes values in unrelated
specs, or the function name threaded through three methods of the plugin base class. Left out
deliberately and stated in
known-limitations.md.