From 1bb9db550f7a360cb13a3aa39d87d4d6c46930db Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 9 Sep 2026 03:37:05 +0000 Subject: [PATCH 1/9] HF-131: report which function produced an error, and which argument 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 --- CHANGELOG.md | 1 + src/Cell.ts | 8 +++++++- src/CellValue.ts | 22 ++++++++++++++++++++++ src/interpreter/plugin/FunctionPlugin.ts | 4 ++-- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f4e12ea23..a0a6f4e58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#131](https://github.com/handsontable/hyperformula/issues/131) +- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#131](https://github.com/handsontable/hyperformula/issues/131) ### Changed diff --git a/src/Cell.ts b/src/Cell.ts index 1bcd808ab1..1b84db3343 100644 --- a/src/Cell.ts +++ b/src/Cell.ts @@ -179,10 +179,16 @@ export class CellError { * propagated error never acquires one: the reading function's own argument slot is not the * offending argument. * + * Also a no-op once `originFunction` is set, even for a non-propagated error: a nested call's + * own error (e.g. `SQRT(-1)` inside `=DATE(1,1,SQRT(-1))`) stamps its origin before the outer + * call's coercion loop ever sees it, and that loop's own argument slot is not the one that + * actually produced the error — attaching an index here would pair someone else's origin with + * this call's argument position, which is incoherent. + * * @param {number} index - zero-based index of the offending argument */ public withArgumentIndex(index: number): CellError { - if (this.propagated || this.argumentIndex !== undefined) { + if (this.propagated || this.originFunction !== undefined || this.argumentIndex !== undefined) { return this } return new CellError(this.type, this.message, this.root, this.originFunction, index, this.propagated, this.originAddress) diff --git a/src/CellValue.ts b/src/CellValue.ts index f672a3f6f4..7579e952c0 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -22,6 +22,26 @@ export class DetailedCellError { */ public readonly hasMessage: boolean + /** + * The function or operator that produced this error, e.g. `'SUM'` or `'divide'`. + * + * `undefined` when no function or operator produced it — a value typed directly into a + * cell, a parsing error, or an error read from another cell without originating here. + * First occurrence wins: a function that only reads or propagates an error never claims + * to have produced it. + */ + public readonly originFunction?: string + + /** + * The zero-based index of the argument that was rejected, when `originFunction` names a + * function whose own argument coercion produced this error. + * + * `undefined` whenever the error was not a coercion failure on one of the origin + * function's own arguments — including when it came from a nested call (its own + * `originFunction` already claimed it) or was propagated from elsewhere. + */ + public readonly argumentIndex?: number + constructor( error: CellError, public readonly value: string, @@ -30,6 +50,8 @@ export class DetailedCellError { this.type = error.type this.message = error.message ?? '' this.hasMessage = error.message !== undefined + this.originFunction = error.originFunction + this.argumentIndex = error.argumentIndex } public toString(): string { diff --git a/src/interpreter/plugin/FunctionPlugin.ts b/src/interpreter/plugin/FunctionPlugin.ts index 810f140479..79f095e212 100644 --- a/src/interpreter/plugin/FunctionPlugin.ts +++ b/src/interpreter/plugin/FunctionPlugin.ts @@ -485,11 +485,11 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck Date: Mon, 14 Sep 2026 10:07:05 +0000 Subject: [PATCH 2/9] HF-131: describe the identities originFunction actually reports 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) --- src/CellValue.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/CellValue.ts b/src/CellValue.ts index 7579e952c0..2ce6ed15b0 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -23,12 +23,22 @@ export class DetailedCellError { public readonly hasMessage: boolean /** - * The function or operator that produced this error, e.g. `'SUM'` or `'divide'`. + * What produced this error. * - * `undefined` when no function or operator produced it — a value typed directly into a - * cell, a parsing error, or an error read from another cell without originating here. - * First occurrence wins: a function that only reads or propagates an error never claims - * to have produced it. + * Usually the function or operator that rejected a value, e.g. `'SUM'` or `'divide'`. + * Errors that exist before any function reads them name what built them instead: + * `'reference'` for a reference that cannot be resolved, `'removed reference'` for one + * destroyed by removing rows or columns, `'parser'` for a formula that could not be + * parsed, `'user input'` for an error value typed into a cell, and `'literal'` for one + * written into a formula. + * + * First occurrence wins, so a function that only read the error never replaces that: + * `=SUM(SQRT(-1))` reports `'SQRT'`, and `=SUM(A2:A99999999999)` reports `'reference'`. + * + * `undefined` when nothing produced the error in this sense — `#SPILL!` and `#CYCLE!` + * arise from the layout of a sheet rather than from evaluating a value. It is also + * `undefined`, and the reading function's name may appear instead, for an error that + * arises inside an array result; see the known limitations. */ public readonly originFunction?: string @@ -38,7 +48,11 @@ export class DetailedCellError { * * `undefined` whenever the error was not a coercion failure on one of the origin * function's own arguments — including when it came from a nested call (its own - * `originFunction` already claimed it) or was propagated from elsewhere. + * `originFunction` already claimed it), when it was propagated from elsewhere, or when + * the argument was a reference that could not be resolved, which the reference itself + * reports. An error raised per element while a function is applied across an array is + * the one case where an index can appear without an `originFunction`; see the known + * limitations. */ public readonly argumentIndex?: number From 2452d5f83386eaf276550d33aead471bd63ec65c Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 14 Sep 2026 10:09:02 +0000 Subject: [PATCH 3/9] HF-131: link the changelog entry to the issue it closes GitHub #131 is an unrelated, closed issue; this work is tracked by #1547. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cee2446aa6..470a045054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#131](https://github.com/handsontable/hyperformula/issues/131) -- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#131](https://github.com/handsontable/hyperformula/issues/131) +- Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#1547](https://github.com/handsontable/hyperformula/issues/1547) +- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#1547](https://github.com/handsontable/hyperformula/issues/1547) ### Changed From 624a8e40d7fe31cfec4e7c0f888f72f3345c80fd Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 14 Sep 2026 15:38:36 +0000 Subject: [PATCH 4/9] HF-131: drop the array exception from the JSDoc, now that it is fixed Co-Authored-By: Claude Opus 5 (1M context) --- src/CellValue.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/CellValue.ts b/src/CellValue.ts index 1929546414..6f56190211 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -38,9 +38,7 @@ export class DetailedCellError { * `=SUM(SQRT(-1))` reports `'SQRT'`, and `=SUM(A2:A99999999999)` reports `'reference'`. * * `undefined` when nothing produced the error in this sense — `#SPILL!` and `#CYCLE!` - * arise from the layout of a sheet rather than from evaluating a value. It is also - * `undefined`, and the reading function's name may appear instead, for an error that - * arises inside an array result; see the known limitations. + * arise from the layout of a sheet rather than from evaluating a value. */ public readonly originFunction?: string @@ -52,9 +50,8 @@ export class DetailedCellError { * function's own arguments — including when it came from a nested call (its own * `originFunction` already claimed it), when it was propagated from elsewhere, or when * the argument was a reference that could not be resolved, which the reference itself - * reports. An error raised per element while a function is applied across an array is - * the one case where an index can appear without an `originFunction`; see the known - * limitations. + * reports. When a function is applied across an array, an element that fails coercion + * carries the index alongside the function's own name, like any other coercion failure. */ public readonly argumentIndex?: number From 223711ddd3a31a55ff7e867e0f732ce4312c2224 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 22 Sep 2026 01:49:14 +0000 Subject: [PATCH 5/9] Report the argument the user wrote, not its position after range expansion @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) --- CHANGELOG.md | 2 +- src/CellValue.ts | 14 +++++--- src/interpreter/plugin/FunctionPlugin.ts | 44 +++++++++++++++++------- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 470a045054..c1bae7d82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#1547](https://github.com/handsontable/hyperformula/issues/1547) -- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#1547](https://github.com/handsontable/hyperformula/issues/1547) +- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. The index counts the arguments as written in the formula, so it does not shift when a preceding argument is a range covering many cells. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#1547](https://github.com/handsontable/hyperformula/issues/1547) ### Changed diff --git a/src/CellValue.ts b/src/CellValue.ts index 6f56190211..8535c352cc 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -46,12 +46,18 @@ export class DetailedCellError { * The zero-based index of the argument that was rejected, when `originFunction` names a * function whose own argument coercion produced this error. * + * The index counts the arguments AS WRITTEN in the formula. For a function that accepts + * ranges, a range is one argument however many cells it covers, so `=AND(A1:A100,"x")` + * reports `1` for `"x"` — the index describes the formula, not the data behind it. + * * `undefined` whenever the error was not a coercion failure on one of the origin * function's own arguments — including when it came from a nested call (its own - * `originFunction` already claimed it), when it was propagated from elsewhere, or when - * the argument was a reference that could not be resolved, which the reference itself - * reports. When a function is applied across an array, an element that fails coercion - * carries the index alongside the function's own name, like any other coercion failure. + * `originFunction` already claimed it), when it was propagated from elsewhere, when the + * argument was a reference that could not be resolved, which the reference itself + * reports, or when the offending value sat INSIDE a range that the function ignores + * (`=AND(A1:A2,TRUE())` over text returns `TRUE`, so there is no error to attribute). + * When a function is applied across an array, an element that fails coercion carries the + * index alongside the function's own name, like any other coercion failure. */ public readonly argumentIndex?: number diff --git a/src/interpreter/plugin/FunctionPlugin.ts b/src/interpreter/plugin/FunctionPlugin.ts index 79f095e212..d005e7d1f0 100644 --- a/src/interpreter/plugin/FunctionPlugin.ts +++ b/src/interpreter/plugin/FunctionPlugin.ts @@ -296,19 +296,27 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck { const isSyntacticallyEmpty = argAst.type === AstNodeType.EMPTY const value = this.evaluateAst(argAst, state) if (value instanceof SimpleRangeValue) { for (const scalarValue of value.valuesFromTopLeftCorner()) { - ret.push([scalarValue, true, isSyntacticallyEmpty]) + ret.push([scalarValue, true, isSyntacticallyEmpty, argumentIndex]) } } else { - ret.push([value, false, isSyntacticallyEmpty]) + ret.push([value, false, isSyntacticallyEmpty, argumentIndex]) } - } + }) return ret } @@ -410,6 +418,10 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck value) const argumentIgnorableFlags = evaluatedArguments.map(([, ignorable]) => ignorable) const syntacticallyEmptyFlags = evaluatedArguments.map(([, , empty]) => empty) + // Which argument each value came from. Identical to the position in this list unless a + // range was expanded above; a custom plugin overriding `listOfScalarValues` with the + // old three-element tuple yields undefined here and falls back to the position. + const sourceArgumentIndexes = evaluatedArguments.map(([, , , index]) => index) const argumentMetadata = this.buildMetadataForEachArgumentValue(argumentValues.length, metadata) const isVectorizationOn = state.arraysFlag && !metadata.vectorizationForbidden @@ -421,13 +433,13 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck [ ...Array(resultArrayWidth).keys() ].map(col => { const vectorizedArguments = this.vectorizeAndBroadcastArgumentsIfNecessary(isVectorizationOn, argumentValues, argumentMetadata, row, col) - const result = this.calculateSingleCellOfResultArray(state, vectorizedArguments, argumentMetadata, argumentIgnorableFlags, syntacticallyEmptyFlags, functionImplementation, metadata.returnNumberType) + const result = this.calculateSingleCellOfResultArray(state, vectorizedArguments, argumentMetadata, argumentIgnorableFlags, syntacticallyEmptyFlags, functionImplementation, metadata.returnNumberType, sourceArgumentIndexes) if (result instanceof SimpleRangeValue) { throw new Error('Function returning array cannot be vectorized.') @@ -448,8 +460,9 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck InterpreterValue, returnNumberType: NumberType | undefined, + sourceArgumentIndexes: number[] = [], ): RawInterpreterValue { - const coercedArguments = this.coerceArgumentsToRequiredTypes(state, vectorizedArguments, argumentsMetadata, argumentIgnorableFlags, syntacticallyEmptyFlags) + const coercedArguments = this.coerceArgumentsToRequiredTypes(state, vectorizedArguments, argumentsMetadata, argumentIgnorableFlags, syntacticallyEmptyFlags, sourceArgumentIndexes) if (coercedArguments instanceof CellError) { return coercedArguments @@ -465,8 +478,13 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck[] { const coercedArguments: Maybe[] = [] + // The argument the user wrote, which is the position in this loop only when no range + // was expanded. Reporting the loop index made the reported argument depend on how many + // cells a preceding range happened to cover. + const writtenArgumentIndex = (i: number): number => sourceArgumentIndexes[i] ?? i for (let i = 0; i < argumentsMetadata.length; i++) { const argumentMetadata = argumentsMetadata[i] @@ -485,11 +503,11 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck [this.evaluateAst(ast, state), false, ast.type === AstNodeType.EMPTY]) + : args.map((ast, argumentIndex) => [this.evaluateAst(ast, state), false, ast.type === AstNodeType.EMPTY, argumentIndex]) } protected buildMetadataForEachArgumentValue(numberOfArgumentValuesPassed: number, metadata: FunctionMetadata): FunctionArgument[] { From 45c10465ae2c5f5fedbe2d9c30772769a8088587 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 22 Sep 2026 02:27:31 +0000 Subject: [PATCH 6/9] Document that a bad cell inside a range is blamed on the range 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) --- CHANGELOG.md | 2 +- src/CellValue.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bae7d82d..423bf08d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#1547](https://github.com/handsontable/hyperformula/issues/1547) -- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. The index counts the arguments as written in the formula, so it does not shift when a preceding argument is a range covering many cells. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#1547](https://github.com/handsontable/hyperformula/issues/1547) +- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. The index counts the arguments as written in the formula, so it does not shift when a preceding argument is a range covering many cells, and a bad value inside a range is attributed to that range's own argument. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#1547](https://github.com/handsontable/hyperformula/issues/1547) ### Changed diff --git a/src/CellValue.ts b/src/CellValue.ts index 8535c352cc..63c3e54940 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -54,8 +54,14 @@ export class DetailedCellError { * function's own arguments — including when it came from a nested call (its own * `originFunction` already claimed it), when it was propagated from elsewhere, when the * argument was a reference that could not be resolved, which the reference itself - * reports, or when the offending value sat INSIDE a range that the function ignores + * reports, or when the offending value sat inside a range that the function IGNORES * (`=AND(A1:A2,TRUE())` over text returns `TRUE`, so there is no error to attribute). + * + * When a function does NOT ignore it — `MULTINOMIAL` coerces every value to a number — + * a bad cell inside a range is attributed to the RANGE's own argument slot, since that + * is the argument the user wrote: `=MULTINOMIAL(1,A1:A3)` with text in `A3` reports `1`. + * The index names the argument to look at, not the cell; the address of the offending + * cell is not part of this field. * When a function is applied across an array, an element that fails coercion carries the * index alongside the function's own name, like any other coercion failure. */ From efefce383c775f7ffd84f53d1a1a8bea9a4f981d Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 22 Sep 2026 04:11:40 +0000 Subject: [PATCH 7/9] Make the argumentIndex JSDoc true on THIS branch, not on the finished 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 624a8e40d, so neither is caused by this change -- but 624a8e40d 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) --- src/CellValue.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/CellValue.ts b/src/CellValue.ts index 63c3e54940..303e27d16b 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -52,9 +52,8 @@ export class DetailedCellError { * * `undefined` whenever the error was not a coercion failure on one of the origin * function's own arguments — including when it came from a nested call (its own - * `originFunction` already claimed it), when it was propagated from elsewhere, when the - * argument was a reference that could not be resolved, which the reference itself - * reports, or when the offending value sat inside a range that the function IGNORES + * `originFunction` already claimed it), when it was propagated from elsewhere, or when + * the offending value sat inside a range that the function IGNORES * (`=AND(A1:A2,TRUE())` over text returns `TRUE`, so there is no error to attribute). * * When a function does NOT ignore it — `MULTINOMIAL` coerces every value to a number — @@ -62,8 +61,14 @@ export class DetailedCellError { * is the argument the user wrote: `=MULTINOMIAL(1,A1:A3)` with text in `A3` reports `1`. * The index names the argument to look at, not the cell; the address of the offending * cell is not part of this field. - * When a function is applied across an array, an element that fails coercion carries the - * index alongside the function's own name, like any other coercion failure. + * + * Two cases currently report an index that is less useful than it looks, both measured: + * an argument that is a reference the engine cannot resolve is claimed by the function + * reading it (`=DATE(2020,A99999999999,1)` reports `DATE` with index `1`, rather than the + * reference reporting itself); and an error raised PER ELEMENT while a function is + * applied across an array carries an index with no `originFunction` at all + * (`=DATE(2020,A1:A2,1)` over text, under array arithmetic). Both are addressed later in + * this change set, not here. */ public readonly argumentIndex?: number From f5ad98a801ab81bdaab0482f08a414b474e13af4 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 22 Oct 2026 17:14:26 +0000 Subject: [PATCH 8/9] docs: shorten the changelog entry to one sentence Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 423bf08d4c..dc83b09f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#1547](https://github.com/handsontable/hyperformula/issues/1547) -- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, so a consumer can tell which function or operator produced a cell error and, when it failed coercing one of its own arguments, which argument that was. The index counts the arguments as written in the formula, so it does not shift when a preceding argument is a range covering many cells, and a bad value inside a range is attributed to that range's own argument. First occurrence wins: a function or operator that only reads or propagates an error never claims to have produced it. [#1547](https://github.com/handsontable/hyperformula/issues/1547) +- Added `DetailedCellError.originFunction` and `DetailedCellError.argumentIndex`, which name the function or operator that produced a cell error and, when it failed to coerce an argument, that argument's position as written in the formula. [#1547](https://github.com/handsontable/hyperformula/issues/1547) ### Changed From 86396098e1082a9d1437cdc9a7451d66949ecaa7 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 23 Oct 2026 00:22:29 +0000 Subject: [PATCH 9/9] HF-131: Describe in the originFunction and argumentIndex JSDoc only what 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 --- hyperformula/src/CellValue.ts | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/hyperformula/src/CellValue.ts b/hyperformula/src/CellValue.ts index 303e27d16b..412bf8056c 100644 --- a/hyperformula/src/CellValue.ts +++ b/hyperformula/src/CellValue.ts @@ -25,20 +25,16 @@ export class DetailedCellError { public readonly hasMessage: boolean /** - * What produced this error. + * What produced this error: the function or operator that rejected a value, e.g. `'SQRT'` + * or `'divide'`. An operator is named after the arithmetic helper it dispatches to. * - * Usually the function or operator that rejected a value, e.g. `'SUM'` or `'divide'`. - * Errors that exist before any function reads them name what built them instead: - * `'reference'` for a reference that cannot be resolved, `'removed reference'` for one - * destroyed by removing rows or columns, `'parser'` for a formula that could not be - * parsed, `'user input'` for an error value typed into a cell, and `'literal'` for one - * written into a formula. + * First occurrence wins, so a function that only received the error from a nested call + * never replaces it: `=SUM(SQRT(-1))` reports `'SQRT'`. An error read from another cell + * keeps the origin it had there. * - * First occurrence wins, so a function that only read the error never replaces that: - * `=SUM(SQRT(-1))` reports `'SQRT'`, and `=SUM(A2:A99999999999)` reports `'reference'`. - * - * `undefined` when nothing produced the error in this sense — `#SPILL!` and `#CYCLE!` - * arise from the layout of a sheet rather than from evaluating a value. + * `undefined` when no function or operator produced the error, e.g. for `#SPILL!`, + * `#CYCLE!`, a parsing error, or an error value typed into a cell. An error from a + * reference the engine cannot resolve is attributed to the function that reads it. */ public readonly originFunction?: string @@ -62,13 +58,11 @@ export class DetailedCellError { * The index names the argument to look at, not the cell; the address of the offending * cell is not part of this field. * - * Two cases currently report an index that is less useful than it looks, both measured: - * an argument that is a reference the engine cannot resolve is claimed by the function - * reading it (`=DATE(2020,A99999999999,1)` reports `DATE` with index `1`, rather than the - * reference reporting itself); and an error raised PER ELEMENT while a function is - * applied across an array carries an index with no `originFunction` at all - * (`=DATE(2020,A1:A2,1)` over text, under array arithmetic). Both are addressed later in - * this change set, not here. + * Two cases report an index that is less useful than it looks: an argument that is a + * reference the engine cannot resolve is claimed by the function reading it + * (`=DATE(2020,A99999999999,1)` reports `DATE` with index `1`), and an error raised per + * element while a function is applied across an array carries an index with no + * `originFunction` (`=DATE(2020,A1:A2,1)` over text, with array arithmetic on). */ public readonly argumentIndex?: number