zip2 (binary operators, TEXT), zip3 (array IF) and MAP now compute the
broadcast cell count with checked multiplication and return #NUM! when it
exceeds MAX_CELLS, instead of allocating (abort) or wrapping on wasm32.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every area, array and value of one SUBTOTAL/AGGREGATE call is charged against MAX_CELLS before any cell is read; past it the call returns #NUM!. A value stored in XFD1048576 no longer makes SUBTOTAL(9,A:XFD) read ~17 billion cells into a vector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
along: =1+1+... with 4,096 operands (8,192 characters, exactly Excel's
limit, which Excel accepts and calculates) overflowed the stack when
entered, because the derived Clone of Expr recursed once per operand
down the chain's left spine. Saving hit the same in the XLSX writer.
- Expr: Clone, PartialEq, walk and map follow the left spine in a loop
and recurse only into right operands and real nesting.
- XLSX: _xlpm. prefixing and the implicit-intersection scan follow the
spine too. The scan's depth limit of 64 wrote any chain of more than
about 64 operands as a dynamic-array formula; such sums are plain
formulas again (ranges in the chain still get array evaluation).
- Evaluate Formula borrows sub-expressions instead of cloning each
operator's subtree (quadratic memory on a long chain).
- Spanish formula entry and display no longer refuse chains of more
than 256 operands; the left spine does not count as nesting.
Tests run on a 1 MiB stack (the Windows main thread) with Excel's
observed results: formula (clone/eq/walk/map/adjust at 4,096), calc
(401 and 4,096 operands, -, *, /, ^, &, %, comparisons, 1,500 refs),
xlsx (long chains written as scalar formulas), engine (cell.set, fill,
copy/paste, insert rows/columns, undo/redo, Evaluate Formula, XLSX save
and reopen), Spanish entry. Each fails without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sheet names starting with '.' or reading as R1C1 (R, C, RC, R1C1...) were saved
unquoted, so Excel could not open the file; PivotTable sources and ADDRESS left
cell-like names (A1) bare. The quoting rule, unquoting and sheet splitting now
live once in gridcraft_core::addr and replace the copies in the formula printer,
pivot, ADDRESS, chart, xlsx, Solver, Go To and the Name Box (which now reads ''
as ').
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lower-casing then upper-casing made dotless ı equal to i (both become I),
which Unicode's default case folding keeps apart; LET(i,1,ı,2,i) could
return the ı binding. same_name now lower-cases and adds the folds lower
case misses (ß/ẞ -> ss, ſ -> s, ς -> σ). Tests cover ı/i and the Greek
final sigma.
- gridcraft_formula::same_name lower-cases then upper-cases each
character, so ß matches ẞ (to_uppercase alone maps ß to SS).
- Formula-bar input uses it too, so a binding such as ſi is not
translated to IF while the evaluator binds it.
- Tests: ISOMITTED with a case variant, ß/ẞ, and the Spanish input case.
Excel matches LET and LAMBDA names case-insensitively in every script,
so =LET(dług,100,DŁUG*2) is 200. The evaluator compared them ASCII-only
and returned #NAME?. The three lookups (name evaluation, ISOMITTED, a
LET variable shadowing a function name) now fold Unicode case.
A formula at the nesting limit reading many uncalculated formulas (for
example SUM over a wide range) suspended the abandoned evaluations once per
cell read, restarting them each time. The other formulas read at the limit
are now collected and calculated together with the suspended one, before the
abandoned evaluations restart. Results, circular-reference and iteration
behaviour are unchanged. Adds a regression test (3000 suspensions before, 1
after).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The deref doc comment still said blocks are trimmed to the used range. Describe
the actual rule: blocks up to FULL_SIZE_CELLS, or up to FULL_SIZE_BOUNDED_CELLS
when they do not run to the sheet's edge, keep their full shape; the rest stop
at the used range. Docs only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
It returned an empty string for every workbook, saved or not, so the classic
sheet-name formula `MID(CELL("filename",A1),FIND("]",…)+1,31)` gave #VALUE!:
with no `]` in the string there is nothing for FIND to find. Excel returns
`<folder>\[<file>]<sheet>`, and empty only before the first save.
The model had no file to report. `source_dir` could not serve: it names the
folder relative links to other workbooks are relative to, and must keep naming
it after a Save As into another folder, because the links held in memory are
still written for the old one — only the saved copy is rebased. `source_path`
is therefore its own field, with that distinction written down.
It is set when a workbook is opened, from the path made absolute so the folder
is the one the user sees, and when a full-fidelity save makes a file the
document's own — before the `saved` snapshot, or the workbook would read as
dirty the moment it was saved, and only when it changes, so an ordinary Save
does not clone the workbook for nothing. A workbook that arrived as bytes knows
its name but never a folder and reports `[name]Sheet`, which still splits on the
`]`; an import reports the file it came from, as Excel does. The sheet is the one
the reference is on, not the active one.
CELL is volatile and already in the volatile sets, so a cell holding it answers
from the next recalculation; after a Save As that means F9 rather than the save
itself, which the engine test asserts rather than papers over. Making a save
recalculate is a larger decision than this issue.
`check` still skips CELL("filename") as volatile, as docs/cli.md says: the
cached value in a file is the folder of the machine that saved it.
`C1#` gave `#REF!` while the dynamic-array formula in C1 happened to return a
single value -- a FILTER matching one row, a SORT or UNIQUE over one item -- so
`ROWS(C1#)` and `SUM(C1#)` failed and `COUNT(C1#)` gave 0. Every formula that
counts or sums a filtered list therefore broke the moment the list had exactly
one entry. In Excel the formula is the anchor of a 1 x 1 range and the three
give 1, the value, and 1.
A pass recorded the spill only for an array larger than one cell:
if a.rows > 1 || a.cols > 1 {
self.spills.insert(k, a.clone());
}
so a 1 x 1 result left no range for `spill_range` to answer with, and `eval`
turned the missing range into `#REF!`. It is recorded unconditionally now.
The report suggested keying this on the cell carrying `cm`, which turned out not
to be expressible: nothing in the model says "dynamic array" positively -- the
reader tells one from a legacy CSE formula only by leaving `Formula::array`
unset. The arm itself is the better test, because a formula whose result is a
scalar never reaches it: `=1+1` does not become an anchor, which a test pins.
It also matches Excel 365, where `=A1:A1` is a 1 x 1 anchor of its own.
When a dynamic array's anchor gets a new formula, the formula is evaluated
while the old array is still laid out, so it can read the old spilled
values. The anchor was then never evaluated again: it is left out of its
own spill area's dependents, so as not to read its own array.
- `A1` was `=SEQUENCE(3)` and becomes `=SUM(A2:A3)+1`: it showed 6 (2 + 3
+ 1) with A2:A3 empty; recalculating from scratch gives 1.
- `A1` was `=SEQUENCE(3)` and becomes `=SEQUENCE(1,A2+1)`: it read A2 as 2
and spilled over A1:C1 instead of showing 1.
The anchor is now evaluated again when its old array is gone or partly
freed. It is still left out when its new array covers the old one, where
it would read its own array.
Regression test in calc
(`an_anchor_given_a_formula_that_reads_its_old_spill_area_sees_it_empty`:
the new formula stops spilling, spills elsewhere, or spills over part of
the old area); it fails without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When two dynamic arrays of one recalculation want the same empty cells,
both look free while the pass runs, so formulas evaluated in that pass
read both arrays. The one laid out second becomes #SPILL! at write-back,
but nothing evaluated its readers again, and they kept its values.
With B1 `=SEQUENCE(3)` and A2 `=SEQUENCE(1,3)` (both want B2), a full
recalculation (opening a file, Calculate Now) leaves A2 at #SPILL!, yet
`=A2*10` showed 10, `=SUM(A2#)` 6, and `=C2` and `=SUM(C2:C3)` 3, though C2
is empty. Entering the same formulas one at a time gave the right values.
An array blocked at write-back now sends the formulas that read its anchor
or the area it would have spilled into to the next round, where they read
#SPILL! and empty cells.
Regression test in calc
(`formulas_reading_an_array_that_loses_a_spill_conflict_read_spill_errors`);
it fails without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reference over 16,384 cells was cut to the sheet's used range when it was
read into an array. Next to a full-size operand the two no longer lined up:
SUMPRODUCT(D3:AZ400*B3:B400,(A3:A400=1)*(D1:AZ1=2)) gave #VALUE!,
D3:AZ400*B3:B400 filled with #N/A, and ROWS, COUNTBLANK and --(r="") lost
the blanks past the data. Excel keeps the reference's size.
Bounded references now keep their full shape up to one column's worth of
cells (1,048,576). References that run to the edge of the sheet (A:A,
A2:A1048576, several whole rows) and larger blocks still stop at the used
range, so whole-column formulas stay fast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A formula that reads one later in row-major order (further down, or on a
later sheet) is calculated on demand, inside the formula that reads it.
Each level costs stack, so real models with chains of about 1,000 such
formulas overflowed the 1 MB main-thread stack on Windows and crashed on
open. Past 2,000 levels the chain gave #CIRC! instead.
On-demand evaluation now goes at most 64 formulas deep. A formula needed
deeper suspends the evaluations in progress: it is calculated from the
top, then the suspended ones again, reading the results kept so far.
Chains of any length calculate, with the same results. A suspended
formula read again is a circular reference, as a formula in progress is,
so loops are found at the same cell as before.
Regression tests build the chains in code: 20,000 formulas each reading
the row below, the same hopping between two sheets, a 5,000-formula loop,
and a loop closing into the middle of a deep chain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MAP, BYROW, BYCOL, SCAN and MAKEARRAY put one value in each cell, but a
LAMBDA returning several values silently gave its first one:
MAP({1,2},LAMBDA(x,{1,2})) was {1,1} and BYROW(…,LAMBDA(r,r)) the first
column. As in Excel, the whole result is now #CALC!, as arrays can't
nest (a single-value array is still fine). MAP with a LAMBDA taking a
different number of parameters than it has arrays, and BYROW or BYCOL
with one taking other than one, ran anyway; Excel gives #VALUE!.
After each round, every array formula that was evaluated queued the
dependents of its spill area again, even when it spilled the same area
with the same values. Two arrays that read each other's spill areas
then queued each other in every round, up to the limit of 8: one edit
evaluated 24 formulas instead of 3. An unchanged spill now leaves its
dependents alone. last_recalc_cells counts the formulas of all rounds.
- restore closing braces the line-level merges of side-by-side tests dropped
- new ChartData literals carry the palette field
- the filter-menu shortcut passes the row with the column
- #688's tab tests account for #851's All Sheets button
- Serbian margins for the ruler-margin labels
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>