Skip to content

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

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

marcin-kordas-hoc wants to merge 17 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

Context

The PR exposes on DetailedCellError which function or operator produced a cell error, and which argument it rejected. It is part 4 of 7 of HF-131, stacked on #1763.

  • Why: HF-131 (2/7): the origin/propagation mechanism, plus the same guarantee outside the interpreter #1762 records originFunction and argumentIndex on the internal CellError, but a consumer could not read them.
  • What changed:
    • hyperformula/src/CellValue.ts: DetailedCellError.originFunction and DetailedCellError.argumentIndex, copied from the CellError, with JSDoc.
    • hyperformula/src/interpreter/plugin/FunctionPlugin.ts: a failed argument coercion records the index of the argument as written. A range keeps its own index for every value expanded out of it.
    • hyperformula/src/Cell.ts: withArgumentIndex does nothing once originFunction is set, so a nested call's error does not get the outer call's index.
  • Result:
Formula originFunction argumentIndex
=SUM(SQRT(-1)) SQRT none
=1/0 divide none
=DATE(2000, "x", 1) DATE 1
=AND(A1:A3, "x"), A1:A3 hold 1 AND 1
=DATE(1, 1, SQRT(-1)) SQRT none

An operator is named after the arithmetic helper it dispatches to, for example divide or addWithEpsilon.

How did you test your changes?

  • Paired tests: handsontable/hyperformula-tests#62 on feat/hf-131-origin-function, with unit/error-argument-index.spec.ts and unit/error-origin-function.spec.ts.
  • These two specs pass against this head (32 tests) and both suites fail against HF-131 (3/7): a consumer can tell 'no message' from 'empty message' #1763's head.
  • CI is green on this head, except the repository-wide FOSSA license check.

Compatibility with Excel and Google Sheets

Not applicable: no formula result changes.

Public API changes

  • DetailedCellError.originFunction?: string and DetailedCellError.argumentIndex?: number (new, read-only).
  • FunctionPlugin.listOfScalarValues and FunctionPlugin.evaluateArguments (protected, used by custom function plugins) return tuples with a fourth element, the written argument index. A plugin that overrides either method with the three-element tuple still runs, but no longer type-checks.

Out of scope and follow-ups

Types of changes

  • Breaking change (existing functionality no longer works as before) – agreed with the maintainers
  • New feature or improvement (a non-breaking change that adds functionality)
  • Bug fix (a non-breaking change that fixes an issue)
  • Additional language file, or a change to an existing language file (translations)
  • Change to the documentation

Related issues

  1. Part of Improve the formula error messages #1547 (HF-131).

Checklist

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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
hyperformula-docs e745be2 Commit Preview URL

Branch Preview URL
Oct 23 2026, 07:17 AM

@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Performance comparison of head (e745be2) vs base (9d436c4)

                                     testName |    base |    head | change
--------------------------------------------------------------------------
                                      Sheet A |   496.6 |  506.76 | +2.05%
                                      Sheet B |  159.98 |  162.89 | +1.82%
                                      Sheet T |  142.96 |  146.29 | +2.33%
                                Column ranges |  491.27 |  493.19 | +0.39%
                                Sorted lookup | 15844.5 | 14540.8 | -8.23%
Sheet A:  change value, add/remove row/column |   16.04 |   17.54 | +9.35%
 Sheet B: change value, add/remove row/column |  144.94 |  149.35 | +3.04%
                   Column ranges - add column |  157.95 |  162.48 | +2.87%
                Column ranges - without batch |  490.79 |  506.77 | +3.26%
                        Column ranges - batch |  123.94 |  126.08 | +1.73%

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

@marcin-kordas-hoc marcin-kordas-hoc left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 223711dd — thanks, the diagnosis was exact.

sourceArgumentIndexes now travels with the expanded values, and coerceArgumentsToRequiredTypes reports writtenArgumentIndex(i) instead of the loop position. On the current head =AND(A1:A3,"x") with A1:A3 filled reports argumentIndex: 1; same for OR and XOR. Pinned in the tests repo by "points at the argument the user wrote, not at its position after range expansion".

Could you re-review when you get a chance? The CHANGES_REQUESTED is the only thing holding this one.

@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

Follow-up since 223711dd: head is now efefce383, paired tests f12e141.

A re-review found the first round of tests could not tell this fix from an implementation that merely clamps the flattened position to the argument count — it passed 16/16. =MULTINOMIAL(A1:A3,5) with text in A2 separates them (clamp gives 1, the written argument is 0); mutants now fail 2 / 1 / 8.

Two things worth knowing: the fix also moved where a bad cell inside a range is attributed (=MULTINOMIAL(1,A1:A3), text in A3: 3 → 1) — covered and documented now; and the fourth tuple element is additive at runtime but not at the type level, so a TypeScript plugin overriding listOfScalarValues or evaluateArguments must widen its return tuple.

@marcin-kordas-hoc

Copy link
Copy Markdown
Collaborator Author

The whole stack is now merged with develop. The range-expansion fix from 223711d still holds on the new head: =AND(A1:A3,"x") with A1:A3 filled reports argumentIndex: 1, pinned in hyperformula-tests#62. Could you take another look when you have a moment?

…hat the code does at this point

The originFunction JSDoc listed origin names that the engine does not produce yet, and the
argumentIndex JSDoc referred to a later change instead of stating the current behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.36%. Comparing base (9d436c4) to head (e745be2).

Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##           feat/hf-131-has-message    #1764      +/-   ##
===========================================================
+ Coverage                    97.34%   97.36%   +0.01%     
===========================================================
  Files                          207      207              
  Lines                        16323    16327       +4     
  Branches                      3621     3624       +3     
===========================================================
+ Hits                         15890    15896       +6     
+ Misses                         425      423       -2     
  Partials                         8        8              
Files with missing lines Coverage Δ
hyperformula/src/Cell.ts 95.31% <100.00%> (ø)
hyperformula/src/CellValue.ts 83.33% <100.00%> (+3.33%) ⬆️
...erformula/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.

This branch has not been deployed

No deployments
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