Skip to content

fix(HF-135): enforce array dimensions and spill boundaries - #1774

Open
Tobiadefami wants to merge 2 commits into
developfrom
fix/HF-135
Open

Tobiadefami wants to merge 2 commits into
developfrom
fix/HF-135

Conversation

@Tobiadefami

@Tobiadefami Tobiadefami commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Context

Task: HF-135. Part of #1646.

SEQUENCE already returned #VALUE! when its dimensions exceed maxRows/maxColumns, but other array-producing paths did not check their size. Oversized intermediate arrays (e.g. =SUM(VSTACK(SEQUENCE(30,1),SEQUENCE(30,1))) with maxRows: 50) were computed silently, and oversized array literals spilled past maxRows.

This PR:

  • Checks predicted array dimensions before mapping cells, and actual dimensions before VSTACK, HSTACK, TRANSPOSE and array-literal concatenation allocate their results. An array that exceeds maxRows or maxColumns returns #VALUE! (Value too large.), including intermediate arrays. Whole-row/column references keep their existing behavior.

  • Returns #SPILL! when an array with valid dimensions would extend past the sheet edge from the formula's position. With maxRows: 4, =SEQUENCE(4) fits at A1 but returns #SPILL! at A2. Excel does the same at its sheet edge (=SEQUENCE(3) in row 1048575 → #SPILL!).

  • Documents both checks in the Arrays guide, and records the difference from Excel and Google Sheets in list-of-differences.md:

    =SUM(VSTACK(SEQUENCE(1048576), SEQUENCE(1))) Result
    HyperFormula (default limits) #VALUE!
    Excel Online #NUM! (rows are capped for in-memory arrays, columns are not)
    Google Sheets 549756338177 (in-memory arrays are not capped)

Why this does not close #1646. The limits apply to each dimension separately, so an array within both limits can still exhaust memory: at the default limits, =SEQUENCE(40000, 18278) (over 731 million cells) passes every check. #1646 stays open for a limit on the total number of cells. The Arrays guide now says so.

Known limitation, tracked separately: an array formula that returns #SPILL! does not spill again when space becomes available (rows removed above it, a blocking value cleared). This is pre-existing for value-blocked arrays; this PR adds a second way into that state (the sheet edge). See #1785, linked from the Arrays guide.

How did you test your changes?

  • Paired tests: handsontable/hyperformula-tests#57 (42 tests). @marcin-kordas-hoc measured that 24 of them fail on the merge-base and all pass on this branch.
  • CI: all 34 checks pass, including the full engine + paired suite under Jest and the browser (Karma) run.
  • Verified the Excel behavior in Excel Online and the Google Sheets behavior in Google Sheets (values in the table above).

Types of changes

  • Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore)
  • 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:

Checklist:

  • I have reviewed the guidelines about Contributing to HyperFormula and I confirm that my code follows the code style of this project.
  • I have signed the Contributor License Agreement.
  • My change is compliant with the OpenDocument standard.
  • My change is compatible with Microsoft Excel.
  • My change is compatible with Google Sheets.
  • I described my changes in the CHANGELOG.md file.
  • My changes require a documentation update.
  • My changes require a migration guide.

Note

Medium Risk
Changes array evaluation and spill error behavior across the interpreter and dependency graph; formulas that previously produced or consumed oversized intermediate arrays will now error instead of computing silently.

Overview
Enforces maxRows and maxColumns on every array result, including intermediates from VSTACK, HSTACK, TRANSPOSE, inline array literals, and size prediction before the dependency graph maps spill cells. Oversized dimensions now return #VALUE! (Value too large.) instead of being built or spilled past limits; wrapping in SUM or ARRAY_CONSTRAIN no longer bypasses this.

Arrays whose size is within config limits but whose spill rectangle would extend past the sheet edge (given the formula position) return #SPILL!, with DependencyGraph.isThereSpaceForArray also treating over-limit predicted ranges as having no spill space. Shared sizing logic lives on ArraySize.exceedsSheetSizeLimits.

Documentation adds an Array size limits section in the arrays guide, a compatibility row in list-of-differences, and a CHANGELOG entry; per-dimension limits still allow very large total cell counts (noted as out of scope for this fix).

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

@cla-external-contractor-signup

Copy link
Copy Markdown

@Tobiadefami thanks for the pull request. No CLA step needed here — our records show you signed the Contributor License Agreement on 2026-07-31. That signature came from our previous signing form and has been carried over, so there is nothing for you to re-sign.

@qunabu

qunabu commented Sep 15, 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 7820d3c. Configure here.

Comment thread CHANGELOG.md Outdated
Guard predicted array dimensions before mapping cells and check actual
dimensions before VSTACK, HSTACK, TRANSPOSE and inline-array concatenation
allocate oversized results. Return #VALUE! when maxRows or maxColumns is
exceeded, including for intermediate arrays. Reuse the existing SEQUENCE
dimension guards and preserve whole-row/column reference behavior.

Also return #SPILL! when valid result dimensions extend beyond the sheet
edge from the formula's position. With maxRows set to 4, SEQUENCE(4) in A2
needs A2:A5 and now returns #SPILL!; the same formula still fits at A1.

Document both boundary checks.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 15, 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 e450ecb Commit Preview URL

Branch Preview URL
Oct 02 2026, 05:56 PM

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Performance comparison of head (e450ecb) vs base (c920375)

                                     testName |   base |    head | change
-------------------------------------------------------------------------
                                      Sheet A | 495.62 |  491.12 | -0.91%
                                      Sheet B | 159.57 |  157.28 | -1.44%
                                      Sheet T | 142.23 |  140.02 | -1.55%
                                Column ranges | 475.75 |  466.96 | -1.85%
                                Sorted lookup |  14707 | 14194.9 | -3.48%
Sheet A:  change value, add/remove row/column |  14.87 |   15.93 | +7.13%
 Sheet B: change value, add/remove row/column | 126.76 |  129.42 | +2.10%
                   Column ranges - add column | 150.83 |  152.01 | +0.78%
                Column ranges - without batch | 456.58 |  467.98 | +2.50%
                        Column ranges - batch | 118.17 |  113.58 | -3.88%

@marcin-kordas-hoc marcin-kordas-hoc 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.

Recommendation: Approve.

I reproduced the remaining scope of GH #1646 on the current base (the SEQUENCE guard itself
already exists on develop; what was missing is the check on intermediate and literal arrays): with
maxRows/maxColumns set to 50/40, =SUM(VSTACK(SEQUENCE(30,1),SEQUENCE(30,1))) silently
computed 930 instead of erroring, and an oversized inline array literal silently spilled past
maxRows instead of erroring — the intermediate array size was never checked when hidden behind a
scalar wrapper or a literal. After this PR, all four cases (direct VSTACK, VSTACK behind SUM,
TRANSPOSE, inline array) correctly return #VALUE!.

I also checked the #SPILL! change independently. The precise behaviour: a SEQUENCE whose spill
range would extend past maxRows/maxColumns now returns #SPILL!, where develop silently
spilled past the limit (maxRows: 4, A2=SEQUENCE(4) → height 5 on develop, #SPILL! here),
while SUM(SEQUENCE(…)) at the same position is unaffected. A spill blocked by a value already
returned #SPILL! on develop and is unchanged.

Measured via MS Graph against a live Excel Online session: Excel returns #SPILL! both for a
spill blocked by a value and for a spill that would run past the sheet edge (SEQUENCE(3) in row
1048575, TRANSPOSE(SEQUENCE(3)) in column XFC), while the scalar SUM(SEQUENCE(3)) is
unaffected — the same three behaviours this PR implements for maxRows/maxColumns. Full engine

  • paired test suite passes (503 suites, 6275 tests, 3 skipped — identical to this PR's CI run).

I checked this against the open thread on #1734 (finite-height staleness breaking column spills
on CHOOSECOLS) since the areas are adjacent. The two do overlap in one place — this PR adds a
range.exceedsSheetSizeLimits(...) check inside DependencyGraph.isThereSpaceForArray(), which
takes an ArrayFormulaVertex — but that is a sheet-size cap, not the finite-height snapshotting
the #1734 bug lives in; choosecolsArraySize, effectiveHeight and ArrayValue.resize are all
untouched here. I merged the two branches locally to confirm the practical answer: the #1734 crash
reproduces identically before and after, same-sheet and cross-sheet. So this PR neither resolves
nor worsens it — that thread stays open and needs its own fix.

@marcin-kordas-hoc marcin-kordas-hoc 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.

My approval stands. Three non-blocking follow-ups from a second pass, all measured on ea75425. I also filed a pre-existing crash I hit while probing as #1780: it is not caused by this PR, and this PR turns part of it into #VALUE!. In the tests PR (hyperformula-tests#57), 24 of 42 tests fail without this fix and all pass with it.

@@ -468,7 +470,7 @@ export class DependencyGraph {

public isThereSpaceForArray(arrayVertex: ArrayFormulaVertex): boolean {
const range = arrayVertex.getRangeOrUndef()

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.

Not a blocker, but worth a follow-up ticket before this ships. Once isThereSpaceForArray returns false here, setNoSpace() replaces the vertex's size with ArraySize.error() (1×1, isRef), and Evaluator.recomputeFormulaVertexValue then short-circuits to setNoSpace() on every later recompute. So a #SPILL! produced by this new branch is permanent until the formula is re-entered or the engine is rebuilt.

Example: with maxRows: 4, =SEQUENCE(4) in A2 is #SPILL!. removeRows(0, [0, 1]) moves it to A1, where it fits, and it stays #SPILL!. The same happens after undo/redo, removeColumns, moveRows, or a dependency change. Excel Online re-spills in that case: deleting row 1 above a #SPILL! SEQUENCE(3) at row 1048575 gives 1,2,3.

The mechanism is pre-existing: a value-blocked array on develop does not re-spill when the blocker is cleared either. But before this change the only way into that state was a collision with a value; now any array near the bottom or right edge gets there. The paired tests only cover recovery by re-entering the formula, updateConfig, or undo of the formula edit, so nothing in the suite observes this. I'd suggest tracking "re-evaluate space for arrays in #SPILL! state after structural changes" as its own task and linking it from the docs paragraph added here.

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.

Filed as #1785, and the Arrays guide links to it in e450ecb20. Before filing I reproduced every case you listed on this branch, with one refinement: redo of the removeRows stays stuck, but undoing the write that blocked the spill recovers. I also confirmed the value-blocked case on develop, and that Excel Online re-spills in all three setups (rows deleted above, a column deleted to the left, a blocking value cleared).

One correction to the scope: the tests#57 undo and dependency-change tests check that the error stays when the array still doesn't fit. Recovery is only covered via editing the formula and updateConfig. The issue says so.

Comment thread docs/guide/arrays.md

## Array rules

### Array size limits

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.

DEV_DOCS.md asks that deviations from Excel be recorded in docs/guide/list-of-differences.md, and this paragraph introduces one. Measured in Excel Online: only rows are capped for in-memory arrays (=SUM(VSTACK(SEQUENCE(1048576),SEQUENCE(1))) → #NUM!); columns are not (=SUM(HSTACK(SEQUENCE(1,16384),SEQUENCE(1,1))) → 134225921, and an over-wide array only fails when it spills, with #SPILL!). HyperFormula caps both dimensions and returns #VALUE!. Capping both is a reasonable safety choice. Could you add a row for it? For example: array dimensions vs. maxRows/maxColumns — HyperFormula: #VALUE! for either dimension, including intermediates; Excel: #NUM! for rows only.

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.

Added an "Array size limits" row to list-of-differences.md in e450ecb20, using your example:

=SUM(VSTACK(SEQUENCE(1048576), SEQUENCE(1)))
HyperFormula (default limits) #VALUE!
Excel Online #NUM!
Google Sheets 549756338177

The Excel column says rows are capped for in-memory arrays but columns aren't, so an over-wide array fails only when it spills, with #SPILL!. I checked Google Sheets directly: it caps neither dimension (=SUM(HSTACK(SEQUENCE(1,18278),SEQUENCE(1,1))) → 167051782, and even a 20-million-cell =SUM(SEQUENCE(20000000)) computes).

Comment thread CHANGELOG.md Outdated

### Fixed

- Fixed array results exceeding the configured row or column limits, including intermediate arrays created by `VSTACK`, `HSTACK`, `TRANSPOSE`, and array literals. Arrays with valid dimensions now return `#SPILL!` when their spill area extends beyond the sheet edge. [#1646](https://github.com/handsontable/hyperformula/issues/1646) [#1774](https://github.com/handsontable/hyperformula/pull/1774)

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.

The limits are per dimension by design (the docs say so), so a formula that respects both limits can still exhaust memory. With {maxRows: 20000, maxColumns: 5000}, a single =SUM(SEQUENCE(20000,5000)) (100 M cells) kills a 1 GB Node heap in about 5 s, and at the defaults =SEQUENCE(40000,18278) (731 M cells) passes every guard.

#1646 is titled "OOM crash", and its last maintainer comment sets the bar at "all the other affected functions". If this PR is meant to close it, the description should say that the remaining exposure is the element count and that it is accepted. Otherwise #1646 should stay open for a total-size cap. Either is fine; it just needs to be written down.

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.

I went with keeping #1646 open. In e450ecb20 the CHANGELOG no longer links #1646, the description says "Part of #1646" and explains why it stays open (=SEQUENCE(40000, 18278) passes every check at the default limits), and the Arrays guide now says an array within both limits can still exhaust memory, using that example.

- list-of-differences: add an "Array size limits" row. HyperFormula caps
  both dimensions with #VALUE!, Excel caps only rows for in-memory
  arrays (#NUM!), Google Sheets caps neither (verified in Excel Online
  and Google Sheets).
- Arrays guide: note that a #SPILL! array does not re-spill when space
  becomes available (#1785), and that per-dimension limits do not cap
  the total number of cells.
- CHANGELOG: drop the #1646 link; this PR is part of #1646, which stays
  open for a total-size limit.

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

codecov Bot commented Oct 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.34%. Comparing base (c920375) to head (e450ecb).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #1774      +/-   ##
===========================================
+ Coverage    97.32%   97.34%   +0.01%     
===========================================
  Files          195      195              
  Lines        15739    15758      +19     
  Branches      3390     3501     +111     
===========================================
+ Hits         15318    15339      +21     
+ Misses         421      411      -10     
- Partials         0        8       +8     
Files with missing lines Coverage Δ
src/ArraySize.ts 100.00% <100.00%> (ø)
src/DependencyGraph/DependencyGraph.ts 98.81% <100.00%> (+<0.01%) ⬆️
src/interpreter/Interpreter.ts 96.07% <100.00%> (+0.71%) ⬆️
src/interpreter/plugin/ArrayPlugin.ts 99.20% <100.00%> (+0.03%) ⬆️
src/interpreter/plugin/MatrixPlugin.ts 98.72% <100.00%> (+0.01%) ⬆️
src/interpreter/plugin/SequencePlugin.ts 98.52% <100.00%> (+0.02%) ⬆️

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

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