Skip to content

fix: predict the array size of a named expression from what it stands for (HF-166, partial) - #1772

Draft
marcin-kordas-hoc wants to merge 2 commits into
developfrom
fix/hf-166-named-range-in-cell
Draft

marcin-kordas-hoc wants to merge 2 commits into
developfrom
fix/hf-166-named-range-in-cell

Conversation

@marcin-kordas-hoc

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

Copy link
Copy Markdown
Collaborator

Context

A named range passed to an array-returning function evaluated to #VALUE! ("Cell range not allowed.") while the same call on the range literal the name stands for worked: =TRANSPOSE(MyRange) failed where =TRANSPOSE(Sheet1!A1:C1) spilled normally. Aggregations over the same name — SUM, COUNT, INDEX — were unaffected throughout.

The cause is in ArraySizePredictor. checkArraySizeForAst had no NAMED_EXPRESSION case, so a named expression fell through to the default branch and was predicted as ArraySize.error(), a 1×1 scalar. The referring cell was therefore never turned into an array vertex, the array-shaped result reached Exporter.exportValue, and that rejects a SimpleRangeValue in a scalar cell with #VALUE!. The prediction now recurses into the expression the name is bound to, isRef included, so a name behaves like what it stands for.

DependencyGraph#getFormulaAst is added for the predictor to reach the expression behind a name, and ArraySizePredictor takes the dependency graph as a constructor argument.

Scope of the behaviour change

Two things change, both in the direction of making a name and the range it stands for agree.

  1. A named range passed to an array-returning function now spills, where it previously errored.
  2. In array arithmetic mode (useArrayArithmetic: true), a named range used as an operand now spills rather than returning #VALUE!. =myRange+1, =myRange>2, =myRange&"x" and their sheet-scoped and horizontal equivalents now produce exactly what =A1:A5+1 and friends produce in the same engine. This was characterized in the test suite as an accepted limitation of the array-size predictor, with a note that no engine follow-up ticket existed yet; this is that follow-up, and the five cases that pinned the old behaviour are updated to assert the two now agree.

Not changed: a bare =MyRange still returns #VALUE!, exactly as a bare =A1:A5 does. Excel spills both. Making bare names spill while bare range literals do not would trade one inconsistency for another, so that difference is left alone and is worth deciding separately.

Two details in the implementation worth calling out. Names may refer to one another, so a set of names currently being predicted guards against a reference cycle recursing until the stack overflows; a name already on it is treated as unpredictable, which leaves the referring cell a scalar formula and lets the evaluator report the cycle as it always did. And the prediction deliberately uses the engine's own useArrayArithmetic setting rather than the calling state's, because a named expression has a cell of its own that is always computed with the engine setting, no matter where the name is used.

Known limitation — this does not yet cover the reported case

A formula that is already present when the engine is built is still not fixed. The array
size of a formula is predicted while the sheet is parsed, and named expressions passed to
buildFromArray / buildFromSheets are registered after that
(src/BuildEngineFactory.ts: graphBuilder.buildGraph(...) runs before
inputNamedExpressions.forEach(...)). At prediction time the name does not resolve, so the
cell is sized as a scalar and never becomes an array vertex, whatever this change does later.

Measured, same engine, same definitions:

how the formula gets there =TRANSPOSE(myName)
present in the array passed to buildFromArray, names passed alongside #VALUE! — unchanged
written with setCellContents after the engine is built spills correctly
name added with addNamedExpression after the build, then the formula spills correctly

So this change helps a sheet that is edited, and not a sheet that is loaded in one go — which
is the more common path and the one in the original report. Reordering the two steps in
BuildEngineFactory is not the fix: tried and measured, it turns every name-referencing
formula into #REF!, because the sheets are still placeholders at that point. Closing the gap
needs a re-sizing pass over name-referencing formulas after the names are registered, which is
a separate, designed change rather than an improvisation on top of this one. The same gap
affects addNamedExpression called after a formula that already refers to the name.

How did you test your changes?

  • Paired test PR: handsontable/hyperformula-tests#56, same branch name so test/fetch-tests.sh picks it up.
  • New specs in that branch: unit/named-expression-array-size.spec.ts, 8 cases. Negative control: 3 of the 8 fail without the engine change; the other 5 are regression guards that pass either way.
  • Five existing cases in unit/named-expressions.spec.ts updated to the new behaviour. Each asserted value was measured against the patched engine, and each compares the named result against the direct range in the same engine rather than only pinning a literal.
  • Full unit suite: 503 suites, 6241 passed, 0 failed, 3 skipped.
  • npm run lint: 0 errors.

Documentation

docs/guide/named-expressions.md stated that "a bare =myRange + 1 does not spill" in array arithmetic mode. That sentence described the defect, so it now says what the engine does; the function-argument bullet gains the array-returning case. The neighbouring claim that =SUM(myRange + 1) returns 20 in array mode was re-measured and still holds, as does every default-mode result on the page.

Status

Opened as a draft: the implementation is complete and green, but release scheduling for this
fix is not decided, so this is not yet a request to merge and no reviewer is requested.

Related issues

  1. HF-166

Types of changes

  • Bug fix (a non-breaking change which fixes an issue)

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • My change is compatible with Microsoft Excel.

🤖 Generated with Claude Code

…s for

A named expression had no case in the array-size predictor, so it fell to the
default and was predicted as a scalar error. The referring cell was therefore
never turned into an array vertex, and an array-shaped result reached the
exporter, which rejects it as a #VALUE! error. A named range passed to an
array-returning function failed this way while the same call on a range literal
worked.

The prediction now recurses into the expression the name is bound to, isRef
included, so a name bound to a range predicts like the range literal it stands
for and a name bound to an array-returning formula predicts like that formula.

Two details worth stating. Names may refer to one another, so a set of names
currently being predicted guards against a reference cycle recursing until the
stack overflows; a name already on it is treated as unpredictable, which leaves
the referring cell a scalar formula and lets the evaluator report the cycle.
And the prediction deliberately uses the engine's own array-arithmetic setting
rather than the calling state's, because the named expression has a cell of its
own that is always computed with the engine setting, no matter where the name
is used.

Adds DependencyGraph#getFormulaAst, which the predictor needs to reach the
expression behind a name.

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

qunabu commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 12, 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 134d2b7 Commit Preview URL

Branch Preview URL
Sep 12 2026, 02:47 PM

The named-expressions guide stated that "a bare =myRange + 1 does not spill" in
array arithmetic mode. That sentence described the defect, so it is replaced by
what the engine now does, and the function-argument bullet gains the
array-returning case that was the reported symptom. The neighbouring claim about
SUM(myRange + 1) returning 20 in array mode was re-measured and still holds, as
does every default-mode result on the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown

Performance comparison of head (134d2b7) vs base (c920375)

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |  489.82 |  505.22 |  +3.14%
                                      Sheet B |  160.85 |  160.86 |  +0.01%
                                      Sheet T |  144.25 |  145.72 |  +1.02%
                                Column ranges |   476.9 |  474.47 |  -0.51%
                                Sorted lookup | 14419.7 | 14139.8 |  -1.94%
Sheet A:  change value, add/remove row/column |   18.43 |   15.28 | -17.09%
 Sheet B: change value, add/remove row/column |  138.62 |  130.77 |  -5.66%
                   Column ranges - add column |  164.64 |  154.01 |  -6.46%
                Column ranges - without batch |  493.59 |  480.94 |  -2.56%
                        Column ranges - batch |  126.03 |   120.5 |  -4.39%

@marcin-kordas-hoc marcin-kordas-hoc changed the title fix: a named range passed to an array-returning function no longer errors (HF-166) fix: predict the array size of a named expression from what it stands for (HF-166, partial) Sep 14, 2026
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.32%. Comparing base (c920375) to head (134d2b7).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop    #1772   +/-   ##
========================================
  Coverage    97.32%   97.32%           
========================================
  Files          195      195           
  Lines        15739    15758   +19     
  Branches      3390     3465   +75     
========================================
+ Hits         15318    15337   +19     
+ Misses         421      413    -8     
- Partials         0        8    +8     
Files with missing lines Coverage Δ
src/ArraySize.ts 100.00% <100.00%> (ø)
src/BuildEngineFactory.ts 100.00% <100.00%> (ø)
src/DependencyGraph/DependencyGraph.ts 98.81% <100.00%> (+<0.01%) ⬆️

... and 5 files 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.

2 participants