recalc_with_range_formulas_scales_linearly is flaky under load, so it is #[ignore]d (run with
--ignored in release). A small range_formulas_recalc_and_bulk_edit_correctly test keeps the
helper's value check in every run. The helper reads Instant::now, so it allows
clippy::disallowed_methods (required once #125's clippy.toml lands).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every edit goes through cmd::commit, which read std::time::Instant::now()
up front for GRIDCRAFT_PROFILE timing, whether profiling was on or not.
wasm32-unknown-unknown has no clock: Instant::now panics there, and with
panic=abort the browser reports `RuntimeError: unreachable`. So in the
web app, typing 1 into A1 and pressing Enter trapped (#118). The native
apps and the CLI have a clock and were unaffected.
commit now reads the clock only when GRIDCRAFT_PROFILE is set, and
never on wasm. The other clock reads (NOW(), the current year for typed
dates, the UI's frame timer) already skip wasm.
Native tests can't see this trap, so clippy.toml now disallows
Instant::now and SystemTime::now. A clock read has to be gated off wasm
and allowed at the call, and `cargo clippy -D warnings` fails on the old
commit code. The existing, already-gated reads and the native-only perf
example allow it.
Checked by building the engine for wasm32-unknown-unknown into a small
cdylib that runs cell.set A1=1 on a new workbook, and running it in
Node: before the fix it traps with `RuntimeError: unreachable`, after
it returns normally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
formula::parse("Sheet1!$D$1#") failed with "unknown error literal".
A # right after a cell reference is now the spill range operator: it
evaluates to the anchor's current spill range, or #REF! when the cell
doesn't spill (or its spill is blocked). Its users depend on the
anchor, so they recalculate when the spill grows or shrinks. Names can
use it.
Files have no # operator: like Excel, the writer stores A1# as
_xlfn.ANCHORARRAY(A1), and the parser reads that back as A1#.
Opening a workbook with a SUM(E:J) on each of 50,000 rows took 9 s
(1.7 s at 25,000, 0.4 s at 12,500), and editing a cell that 50,000
VLOOKUPs read froze the app for 40 s or more. Two things grew with the
number of ranges:
- Evaluating a range trims it to the sheet's used range, and
Sheet::used_range walks every stored row. It ran once per range read,
so a full recalc was rows x range formulas. The pass's host now works
it out once per sheet (the workbook doesn't change during a pass);
Host::used_range keeps the old behaviour for other hosts.
- The reverse index kept each column's range precedents in a Vec:
finding a cell's dependents scanned every range in the column, and
re-entering a formula ran a retain over each of its columns. Pasting
a column, filling formulas down or sorting was quadratic. Each column
(and each sheet's wide ranges) now has an interval index: sorted by
start, with a segment tree of the largest end, so a lookup costs
O(log n + matches). Removals mark entries dead and insertions wait in
a small sorted set; Graph::settle merges them after each batch, so
edits cost O(log n) amortised. Single-cell dependents are a set, so
removing one is O(1). dependents_of_range asks each column once
instead of once per cell.
Semantics are unchanged: whole rows and columns, other sheets, names,
tables, spills and volatile functions go through the same paths.
Release CLI, open + full recalc: 12.5k rows 0.39 s -> 0.06 s, 25k 1.83 s
-> 0.14 s, 50k 9.3 s -> 0.28 s. The 50k x 20 bug-hunt workbook: recalc
on open 87 s -> 1.7 s, editing C2 (100k dependents) 51 s -> 1.3 s;
filling 50k SUMs down 17 s -> 0.18 s, sorting 50k rows 9 s -> 0.14 s.
Tests: the interval index against a linear scan (inserts, removals,
revivals, merges) and its query cost in tree nodes; range, wide,
whole-column, whole-row and cross-sheet dependents after edits; and a
scaling test (open, change a column, re-enter every formula) that
allows 8x the time for 4x the rows. Before the fix it took 16x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Excel adds up the range that starts at sum_range's top-left cell and has
the criteria range's height and width, whatever size sum_range has. With
A1:B3 = a,1 / b,2 / a,3, =SUMIF(A1:A3,"a",B1) is 4; GridCraft gave 1,
because if_numbers only saw sum_range's values (one cell) and read them
at the criteria positions. Its comment promised the resizing, but by then
the argument was a plain value.
The evaluator has the references, so it now does the resizing, as it does
for OFFSET and INDEX: for SUMIF and AVERAGEIF with three arguments, a
single-area sum range reference becomes resized_sum_range(sum, criteria
height, criteria width), cut off at the sheet's edge. It works across
sheets and through names. The dependency graph records the resized
range, so editing a cell inside it recalculates; when the criteria range
isn't a plain reference the formula is recalculated every time instead.
SUMIFS and AVERAGEIFS keep requiring same-size ranges (#VALUE!
otherwise).
Tests: a single-cell, shorter, wider, longer and horizontal sum range,
whole-column criteria, editing a cell of the resized range, the sheet
edge, another sheet and a defined name, and SUMIFS/AVERAGEIFS size
mismatches. They fail before the fix (1 instead of 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ui-egui: title bar with QAT, full ribbon (Home/Insert/Draw/Page Layout/Formulas/Data/
Review/View/Automate + Table/Chart Design), formula bar with Name Box, virtualised grid
(frozen panes, overflow, borders, CF data bars/scales/icons, tables, sparklines, filter
buttons, charts/pictures/shapes), in-cell editor with point mode, reference colouring,
autocomplete and argument hints, sheet tabs, status bar, dialogs, control channel
- apps/sheetcraft: desktop app (control server, file dialogs, prefs, drag & drop)
- chart: toolkit-free chart layout/rendering for 25 chart kinds + sparklines + rasteriser
- mcp + sheetcraft-cli: MCP server (headless or bridged) and CLI (info/convert/eval/cat/run/mcp)
- xtask (ci/layers/assets/wasm/version/parity), release workflows, packaging, placeholder icon
- automatic row heights
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>