Skip to content

feat(formulas): element-wise array operators - #71

Open
chh-ay wants to merge 4 commits into
perf/formula-kernelsfrom
feat/array-arithmetic
Open

chh-ay wants to merge 4 commits into
perf/formula-kernelsfrom
feat/array-arithmetic

Conversation

@chh-ay

@chh-ay chh-ay commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Scope

Element-wise array operators in both engines. Part of #59. Stacked on #70 (base perf/formula-kernels).

Problem

Operators accepted only scalars. =A1:A3*10, =SUM(A1:A3*B1:B3), =FILTER(B1:B12, E1:E12>=1000), and =SUMPRODUCT((A1:A3>5)*B1:B3) returned #VALUE! (or 0) in both engines, and =A1#*10 could not scale a spill.

Change

  • refactor(wasm): unary, binary, and comparison semantics move into eval/operators.rs; scalar evaluation calls them unchanged.
  • feat(formulas): an operator applies element by element when an operand is an array (range, array constant, spill reference, or array result). A scalar, a single row, or a single column expands to match the other operand (Excel broadcasting). Element errors stay in their cells. Array results spill, and functions such as SUM, SUMPRODUCT, INDEX, IF, FILTER, and SORTBY accept them.
  • fix(formulas): a formula classified as possibly array-valued that resolves to a scalar keeps its scalar result (=-LET(x,SEQUENCE(3),1) is -1), and #REF! from an invalid spill reference stays #REF! (=H1#*10).
  • Docs: the formulas guide has a new "Array arithmetic" section, and the spill-reference paragraph now shows =A1#*10.

Behavior change: =VSTACK(B2,A1)+0 now spills both values instead of returning the first one, as in Excel.

Measurement

Interleaved ABBA capture, one fresh process per sample, each side loading its own built package, both locks held, CPU pinned. Untouched 100k independent recompute: +0.21% (bootstrap 95% CI 0.96–1.08), inside the A/A noise band. A sequential control gave −0.44%. An earlier +8.3% reading did not reproduce under either protocol.

Size

Binary Before (7f73dc7) After (8773d2a) Change
Default raw 788,017 793,255 +5,238 (+0.66%)
Default Brotli q11 236,355 238,012 +1,657 (+0.70%)
Full raw 1,070,088 1,076,283 +6,195 (+0.58%)
Full Brotli q11 324,501 326,416 +1,915 (+0.59%)

Verification

  • New Rust tests (broadcast on each axis, element errors, comparisons feeding array functions, range and spill refresh, the scalar and #REF! fixes) fail on 499570a (3 default, 4 analysis) and pass now.
  • Rust 147 (default) and 223 (--features analysis) pass; clippy passes in both configurations.
  • Packages 861 pass / 6 skip / 0 fail. Build, typecheck, lint, api:check, and docs:check pass.
  • Public-package probe: =A1:A3*10 → 10, 20, 30; =SUMPRODUCT((A1:A3>1)*B1:B3) → 50; =FILTER(B1:B12,E1:E12>=1000) spills; before, all three were #VALUE!.

chh-ay added 3 commits October 5, 2026 17:42
The element-wise operator path assumed every operand it reached was an array.
A scalar-producing operand made the unary path report `#VALUE!` instead of the
scalar result, so `=-LET(x,SEQUENCE(3),1)` lost its `-1`, and a spill reference
whose anchor holds no spill replaced the scalar expression's error, so
`=H1#*10` reported `#VALUE!` instead of `#REF!`.

Operands that `eval_dynamic_array` answers with no matrix now evaluate as
scalars, and a resolved spill reference that leaves no array keeps the scalar
error. Adds the regression cases for unary `-`, `+` and `%` over a scalar LET
and for a missing spill, and marks the shared scalar helpers `#[inline]`.

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.

1 participant