Skip to content

HF-131 (4/7): expose which function produced an error, and which argument - #1764

Open
marcin-kordas-hoc wants to merge 13 commits into
feat/hf-131-has-messagefrom
feat/hf-131-origin-function
Open

marcin-kordas-hoc wants to merge 13 commits into
feat/hf-131-has-messagefrom
feat/hf-131-origin-function

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What and why

Stacked on #1763. Exposes on the public DetailedCellError type the two fields the
origin/propagation mechanism (introduced in #1762) already computes internally:
originFunction?: string and argumentIndex?: number — which function or operator produced a
cell error, and, when it failed coercing one of its own arguments, which argument that was.

Revision note: an earlier version of this description said this PR built the mechanism
itself (the two choke points, Cell.ts's builders, etc.). That was wrong — the mechanism landed
in #1762 (see its revised description). This PR only does the public-facing exposure step the
mechanism commit's own message called out as a separate, later concern:

"originFunction and argumentIndex are carried on CellError and stamped in evaluateAst, but
DetailedCellError does not expose them yet... That split is the natural PR boundary here."

This PR is that boundary: expose the two fields, and wire the one direct caller outside the two
choke points.

What actually changes here (4 files, +32/-3):

  • src/CellValue.tsDetailedCellError gains originFunction/argumentIndex, copied from the
    underlying CellError in the constructor. Additive only: enumerable property count on
    DetailedCellError grows from 5 (type/message/value/address/hasMessage from HF-131 (3/7): a consumer can tell 'no message' from 'empty message' #1763) to 7.
  • src/interpreter/plugin/FunctionPlugin.tscoerceArgumentsToRequiredTypes's two error-return
    paths (wrong-type, non-scalar) now call .withArgumentIndex(i) — the only direct callers of
    that builder outside the two choke points HF-131 (2/7): the origin/propagation mechanism, plus the same guarantee outside the interpreter #1762 introduced.
  • src/Cell.tswithArgumentIndex's no-op gate also checks originFunction !== undefined, not
    just argumentIndex: needed so a nested call's own error (SQRT(-1) inside
    =DATE(1,1,SQRT(-1))) doesn't get DATE's argument index attached after already claiming its
    own origin.
  • CHANGELOG.md — documents the two new public fields.

Verified against nested/propagated cases directly: =SUM(SQRT(-1)) reports SQRT, not SUM;
=1/0 then =SUM(A1) reports divide, propagated: true, not re-claimed by SUM;
=DATE(1,1,SQRT(-1)) reports {originFunction: 'SQRT', argumentIndex: undefined}, not a leaked
index on DATE's argument slot.

Known, deliberate limit: a binary operator's error names the operator (addWithEpsilon,
divide, etc.), not which operand failed — argumentIndex is only set for a function's own
coercion loop; the two operands of a binary op have no equivalent loop. Not solved here.

Verified

Stack

4 of 7 — stacked on #1763. Next: fix/hf-131-rootless-origin.

🤖 Generated with Claude Code


Note

Medium Risk
Changes public error metadata and attribution rules during argument coercion; formula results should be unchanged but consumers and diagnostics depend on the new fields.

Overview
Exposes originFunction and argumentIndex on public DetailedCellError, copying them from the internal CellError so callers can see which function/operator produced an error and which formula argument failed coercion.

FunctionPlugin now tracks the user-written argument index through range expansion (listOfScalarValues / evaluateArguments add a 4th tuple field) and passes it into coerceArgumentsToRequiredTypes, which calls .withArgumentIndex(...) on wrong-type and non-scalar coercion failures so indices match the formula (e.g. a range counts as one argument, not N flattened cells).

Cell.withArgumentIndex is tightened so it is a no-op once originFunction is already set, preventing outer functions from attaching their argument slot to errors that nested calls already claimed (e.g. SQRT(-1) inside DATE(...)).

CHANGELOG documents the new fields and related error-metadata work in the HF-131 stack.

Reviewed by Cursor Bugbot for commit efefce3. Bugbot is set up for automated code reviews on this repo. Configure here.

Performance

The hot-path change lives in #1762 and is measured there: total median −0.24% over three alternating
base/head rounds against origin/develop, within the run-to-run spread, with one CRUD benchmark
("Column ranges - add column") consistently about 3% slower. This PR adds fields to an object that
is only built when an error is exported, so it carries no measurable cost of its own.

DetailedCellError.originFunction and .argumentIndex ride along from
CellError, which already carried them internally since the origin/
propagation redesign -- this task is the public-exposure half.

Two gaps found and closed while verifying, not assumed fixed by that
earlier redesign:

- FunctionPlugin.coerceArgumentsToRequiredTypes's two error-return sites
  never called withArgumentIndex, so a genuine per-argument coercion
  failure (e.g. `=DATE(2020,"x",1)`) reported argumentIndex: undefined
  instead of 1. Both sites now call it.
- withArgumentIndex's gate checked only propagated/argumentIndex-already-set,
  missing the same originFunction check withOrigin already has. A nested
  call's own error (SQRT(-1) inside `=DATE(1,1,SQRT(-1))`) stamps its
  origin before DATE's own coercion loop ever sees it; without the extra
  guard that loop would still attach its own argument index to SQRT's
  error -- incoherent, since SQRT never had that argument. Verified via a
  same-formula, no-cell, no-propagation repro (the cross-cell version does
  not reproduce the bug, since the propagated gate alone already covers it).

Every operator/function origin name asserted in the new specs was verified
by direct probe against this worktree before being committed, including
correcting one substituted-in test formula that turned out to exercise a
different operator (multiply, not addWithEpsilon) than intended.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qunabu

qunabu commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1bb9db5. Configure here.

Comment thread CHANGELOG.md Outdated
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
hyperformula-docs efefce3 Sep 22 2026, 04:12 AM

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Performance comparison of head (efefce3) vs base (915ca17)

                                     testName |   base |    head | change
-------------------------------------------------------------------------
                                      Sheet A | 504.04 |  488.77 | -3.03%
                                      Sheet B | 164.09 |  157.19 | -4.21%
                                      Sheet T | 141.31 |  138.36 | -2.09%
                                Column ranges | 532.97 |  523.08 | -1.86%
                                Sorted lookup |  15510 | 15086.7 | -2.73%
Sheet A:  change value, add/remove row/column |  17.08 |   16.46 | -3.63%
 Sheet B: change value, add/remove row/column | 144.63 |  144.09 | -0.37%
                   Column ranges - add column | 166.16 |  161.23 | -2.97%
                Column ranges - without batch | 515.63 |  494.54 | -4.09%
                        Column ranges - batch | 128.63 |  126.57 | -1.60%

@marcin-kordas-hoc marcin-kordas-hoc changed the title HF-131 (4/7): which function produced an error, and which argument HF-131 (4/7): expose which function produced an error, and which argument Sep 11, 2026
marcin-kordas-hoc and others added 7 commits September 14, 2026 09:48
The doc predated the identities the later commits added, so it said a parsing error
or a value typed into a cell reports undefined, when they report 'parser' and
'user input'. It now lists the vocabulary, and names the one case where an argument
index can appear without an origin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub #131 is an unrelated, closed issue; this work is tracked by #1547.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@Tobiadefami Tobiadefami left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we preserve the original argument index through range expansion? With A1:A3 containing 1, =AND(A1:A3,"x") reports argumentIndex: 3, but "x" is the second argument, so the index should be 1. The .withArgumentIndex(i) calls in FunctionPlugin.ts:488–492 use the flattened position.

Carrying the original index alongside each expanded value fixed this for AND, OR, and XOR in a temporary patch. All 164 tests in the selected suites passed, along with TypeScript and lint.

marcin-kordas-hoc and others added 3 commits September 22, 2026 01:49
…nsion

@Tobiadefami on PR #1764: with A1:A3 holding 1, =AND(A1:A3,"x") reported
argumentIndex 3, but "x" is the second argument, so it should be 1.
Reproduced first, before touching anything, and measured at 3.

Cause: for a function with `expandRanges`, listOfScalarValues flattens every
range into its cells before coercion runs, and coerceArgumentsToRequiredTypes
then called withArgumentIndex with its position in that flattened list. The
reported argument therefore depended on how many cells a preceding range
happened to cover -- the same formula over A1:A2 and A1:A5 blamed arguments 2
and 5 for the same "x".

Fix: listOfScalarValues now carries the originating argument index alongside
each scalar, every value expanded out of one range keeping that range's own
index, and runFunction threads the list down to the two withArgumentIndex
call sites. The fourth tuple element is additive: a custom plugin that
overrides listOfScalarValues with the old three-element shape yields
undefined and falls back to the loop position, i.e. today's behaviour.

Measured after the fix, on the same probes:

  =AND(A1:A3,"x")        1  (was 3)
  =AND(A1:A2,B1:B3,"x")  2  (was 5)
  =AND("x",A1:A2)        0  (unchanged -- already correct)
  =AND(TRUE(),"x")       1  (unchanged -- no range involved)

Full suite 507 suites / 6297 tests green; eslint 0 errors and one warning
fewer than before. Paired tests: hyperformula-tests 8604992.

The changelog bullet is extended rather than given a Fixed entry, because
argumentIndex has never shipped -- the guarantee it now states is that the
index counts arguments as written, so it does not shift with the size of a
range. The public JSDoc says the same, and also records the measured case
where there is deliberately no index at all: a bad value INSIDE a range that
the function ignores (=AND(A1:A2,TRUE()) over text returns TRUE).

Worth naming, because it is the third time on this task: every worked example
in the design, the ADR and this file's existing tests used scalar arguments.
Nobody passed a range. Same shape as "first occurrence wins" (every example
was a function inside a function) and originAddress (no CRUD probe was ever
run against the claim).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the independent re-derivation. The fix also changed where a
coercion failure INSIDE a range is attributed, and neither the JSDoc nor the
changelog said so.

The JSDoc's only inside-a-range sentence described the case where a function
IGNORES the value (AND over text returns TRUE, so there is no error). For a
function that coerces every value the error is real, and it is now attributed
to the range's own argument slot rather than to a position derived from cell
count -- which is the same contract, stated for the case the sentence did not
cover. Also says plainly what the field is not: it names the argument to look
at, not the offending cell, whose address this field never carried.

Paired test: hyperformula-tests 275e955.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… stack

Two sentences described behaviour that arrives in 5/7 (fix/hf-131-rootless-origin),
not here. Measured at this branch's tip:

  =DATE(2020,A99999999999,1)  -> {originFunction:'DATE', argumentIndex:1}
     while the JSDoc said an unresolvable reference "reports itself"
  =DATE(2020,A1:A2,1) over text, array arithmetic
     -> every element {argumentIndex:1, originFunction:undefined}
     while the JSDoc said the element "carries the index alongside the
     function's own name"

Both identical at 624a8e4, so neither is caused by this change -- but 624a8e4
is the commit that deleted the known-limitation wording for the second one, with
the message "now that it is fixed", and it was not fixed at that commit. So the
false sentences are this PR's, even though the behaviour is not.

Rewritten to state what the branch does and to name both cases as addressed later
in the change set. That way every sentence shipped is true at every tip of the
stack rather than only at the last one, which is what a per-PR reviewer needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.40%. Comparing base (915ca17) to head (efefce3).

Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##           feat/hf-131-has-message    #1764      +/-   ##
===========================================================
+ Coverage                    97.39%   97.40%   +0.01%     
===========================================================
  Files                          195      195              
  Lines                        15844    15848       +4     
  Branches                      3494     3497       +3     
===========================================================
+ Hits                         15431    15437       +6     
+ Misses                         405      403       -2     
  Partials                         8        8              
Files with missing lines Coverage Δ
src/Cell.ts 95.31% <100.00%> (ø)
src/CellValue.ts 83.33% <100.00%> (+3.33%) ⬆️
src/interpreter/plugin/FunctionPlugin.ts 99.07% <100.00%> (+<0.01%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

3 participants