@devmm/puredocs-excel-formula 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TVE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs CHANGED
@@ -4457,12 +4457,15 @@ function parseFormula(formula) {
4457
4457
  const stripped = formula.startsWith("=") ? formula.slice(1) : formula;
4458
4458
  const cached = astCache.get(stripped);
4459
4459
  if (cached !== void 0) return cached;
4460
- const lexer = new FormulaLexer(stripped);
4461
- const tokens = lexer.tokenize();
4462
- const ast = new FormulaParser(tokens).parse();
4460
+ const ast = parseFormulaUncached(stripped);
4463
4461
  astCache.set(stripped, ast);
4464
4462
  return ast;
4465
4463
  }
4464
+ function parseFormulaUncached(formula) {
4465
+ const stripped = formula.startsWith("=") ? formula.slice(1) : formula;
4466
+ const tokens = new FormulaLexer(stripped).tokenize();
4467
+ return new FormulaParser(tokens).parse();
4468
+ }
4466
4469
  var DEFAULT_LAMBDA_DEPTH = 1024;
4467
4470
  function buildContext(sheet, opts) {
4468
4471
  const evaluating = opts.evaluating ?? /* @__PURE__ */ new Set();
@@ -4486,7 +4489,7 @@ function buildContext(sheet, opts) {
4486
4489
  return ctx;
4487
4490
  }
4488
4491
  function resolveName(name, sheet, workbook, evaluating, resolving, resolved, recursionGuard) {
4489
- const key = `${sheet.name}\0${name.toUpperCase()}`;
4492
+ const key = `${sheet.name} ${name.toUpperCase()}`;
4490
4493
  const cached = resolved.get(key);
4491
4494
  if (cached !== void 0) return cached;
4492
4495
  if (resolving.has(key)) return FormulaValue.errorRef;
@@ -4514,7 +4517,13 @@ function resolveName(name, sheet, workbook, evaluating, resolving, resolved, rec
4514
4517
  function evaluateFormula(formula, sheet, options) {
4515
4518
  if (!formula) return FormulaValue.blank;
4516
4519
  try {
4517
- const ast = parseFormula(formula);
4520
+ return evaluateAst(parseFormula(formula), sheet, options);
4521
+ } catch {
4522
+ return FormulaValue.errorValue;
4523
+ }
4524
+ }
4525
+ function evaluateAst(ast, sheet, options) {
4526
+ try {
4518
4527
  const ctx = buildContext(sheet, {
4519
4528
  workbook: options?.workbook,
4520
4529
  formulaRow: options?.formulaRow,
@@ -4602,6 +4611,95 @@ function refContains(ref, sheet, row2, col) {
4602
4611
  return ref.sheet === sheet && row2 >= ref.startRow && row2 <= ref.endRow && col >= ref.startCol && col <= ref.endCol;
4603
4612
  }
4604
4613
 
4614
+ // src/dependency-index.ts
4615
+ var TILE_ROWS = 64;
4616
+ var TILE_COLS = 64;
4617
+ var MAX_TILES_PER_REF = 32;
4618
+ var _tiles, _wide, _placements, _DependencyIndex_instances, fileIn_fn;
4619
+ var DependencyIndex = class {
4620
+ constructor() {
4621
+ __privateAdd(this, _DependencyIndex_instances);
4622
+ /** `sheet|tileRow|tileCol` → formula keys whose precedents overlap that tile. */
4623
+ __privateAdd(this, _tiles, /* @__PURE__ */ new Map());
4624
+ /** sheet → formula keys with a precedent too broad to tile. */
4625
+ __privateAdd(this, _wide, /* @__PURE__ */ new Map());
4626
+ /** formula key → the bucket keys it was filed under, for cheap removal. */
4627
+ __privateAdd(this, _placements, /* @__PURE__ */ new Map());
4628
+ }
4629
+ /** Registers (or re-registers) a formula's precedent rectangles. */
4630
+ add(formulaKey, refs) {
4631
+ this.remove(formulaKey);
4632
+ if (refs.length === 0) return;
4633
+ const buckets = [];
4634
+ for (const ref of refs) {
4635
+ const tileRow0 = ref.startRow / TILE_ROWS | 0;
4636
+ const tileRow1 = ref.endRow / TILE_ROWS | 0;
4637
+ const tileCol0 = ref.startCol / TILE_COLS | 0;
4638
+ const tileCol1 = ref.endCol / TILE_COLS | 0;
4639
+ const tileCount = (tileRow1 - tileRow0 + 1) * (tileCol1 - tileCol0 + 1);
4640
+ if (tileCount > MAX_TILES_PER_REF) {
4641
+ __privateMethod(this, _DependencyIndex_instances, fileIn_fn).call(this, __privateGet(this, _wide), ref.sheet, formulaKey, buckets, `w ${ref.sheet}`);
4642
+ continue;
4643
+ }
4644
+ for (let tr = tileRow0; tr <= tileRow1; tr++) {
4645
+ for (let tc = tileCol0; tc <= tileCol1; tc++) {
4646
+ const key = `${ref.sheet} ${tr} ${tc}`;
4647
+ __privateMethod(this, _DependencyIndex_instances, fileIn_fn).call(this, __privateGet(this, _tiles), key, formulaKey, buckets, `t ${key}`);
4648
+ }
4649
+ }
4650
+ }
4651
+ __privateGet(this, _placements).set(formulaKey, buckets);
4652
+ }
4653
+ /** Forgets a formula. */
4654
+ remove(formulaKey) {
4655
+ const buckets = __privateGet(this, _placements).get(formulaKey);
4656
+ if (!buckets) return;
4657
+ for (const bucket of buckets) {
4658
+ const wide = bucket.charCodeAt(0) === 119;
4659
+ const store = wide ? __privateGet(this, _wide) : __privateGet(this, _tiles);
4660
+ const key = bucket.slice(2);
4661
+ const set = store.get(key);
4662
+ if (!set) continue;
4663
+ set.delete(formulaKey);
4664
+ if (set.size === 0) store.delete(key);
4665
+ }
4666
+ __privateGet(this, _placements).delete(formulaKey);
4667
+ }
4668
+ /** Drops every registration. */
4669
+ clear() {
4670
+ __privateGet(this, _tiles).clear();
4671
+ __privateGet(this, _wide).clear();
4672
+ __privateGet(this, _placements).clear();
4673
+ }
4674
+ /**
4675
+ * Calls `visit` with the key of each formula that *may* read the given cell.
4676
+ * Callers still confirm with {@link refContains}: a tile is coarser than a
4677
+ * rectangle, so this over-approximates by at most a tile's worth.
4678
+ */
4679
+ candidates(sheet, row2, col, visit) {
4680
+ const tile = __privateGet(this, _tiles).get(
4681
+ `${sheet} ${row2 / TILE_ROWS | 0} ${col / TILE_COLS | 0}`
4682
+ );
4683
+ if (tile) for (const key of tile) visit(key);
4684
+ const wide = __privateGet(this, _wide).get(sheet);
4685
+ if (wide) for (const key of wide) visit(key);
4686
+ }
4687
+ };
4688
+ _tiles = new WeakMap();
4689
+ _wide = new WeakMap();
4690
+ _placements = new WeakMap();
4691
+ _DependencyIndex_instances = new WeakSet();
4692
+ fileIn_fn = function(store, key, formulaKey, buckets, bucketId) {
4693
+ let set = store.get(key);
4694
+ if (!set) {
4695
+ set = /* @__PURE__ */ new Set();
4696
+ store.set(key, set);
4697
+ }
4698
+ if (set.has(formulaKey)) return;
4699
+ set.add(formulaKey);
4700
+ buckets.push(bucketId);
4701
+ };
4702
+
4605
4703
  // src/recalc-engine.ts
4606
4704
  function createWorkbookRecalcModel(workbook) {
4607
4705
  return {
@@ -4634,7 +4732,7 @@ function parseKey(key) {
4634
4732
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(ref);
4635
4733
  return { sheet, ref, row: row2, col: column2 };
4636
4734
  }
4637
- var _model, _registry, _formulas, _sheetNames, _spills, _spillOwner, _RecalcEngine_instances, rememberSheet_fn, recalcFrom_fn, dependentClosure_fn, directDependents_fn, evaluatePass_fn, writeResult_fn, isOccupied_fn, clearSpill_fn, evaluateFormulaValue_fn, sheetLike_fn, buildWorkbookLike_fn;
4735
+ var _model, _registry, _formulas, _sheetNames, _spills, _spillOwner, _dependents, _volatiles, _sheetLikes, _workbookLike, _RecalcEngine_instances, register_fn, unregister_fn, rememberSheet_fn, applyShift_fn, shiftPrecedents_fn, shiftSpillBookkeeping_fn, recalcFrom_fn, runFixpoint_fn, addVolatiles_fn, dependentClosure_fn, forEachDirectDependent_fn, evaluatePass_fn, writeResult_fn, isOccupied_fn, clearSpill_fn, evaluateFormulaValue_fn, sheetLike_fn, buildSheetLike_fn, buildWorkbookLike_fn;
4638
4736
  var RecalcEngine = class {
4639
4737
  constructor(model, registry = FunctionRegistry.default) {
4640
4738
  __privateAdd(this, _RecalcEngine_instances);
@@ -4647,6 +4745,19 @@ var RecalcEngine = class {
4647
4745
  __privateAdd(this, _spills, /* @__PURE__ */ new Map());
4648
4746
  /** spilled cellKey → the anchor cellKey that owns it. */
4649
4747
  __privateAdd(this, _spillOwner, /* @__PURE__ */ new Map());
4748
+ /**
4749
+ * Reverse dependency index, so finding the formulas that read a cell does not
4750
+ * mean testing every registered formula.
4751
+ */
4752
+ __privateAdd(this, _dependents, new DependencyIndex());
4753
+ /**
4754
+ * Keys of the volatile formulas. Maintained incrementally: scanning every
4755
+ * formula for volatility on each edit is O(F) per keystroke on its own.
4756
+ */
4757
+ __privateAdd(this, _volatiles, /* @__PURE__ */ new Set());
4758
+ /** Memoised per-sheet adapters — these are stateless w.r.t. the formula. */
4759
+ __privateAdd(this, _sheetLikes, /* @__PURE__ */ new Map());
4760
+ __privateAdd(this, _workbookLike);
4650
4761
  __privateSet(this, _model, model);
4651
4762
  __privateSet(this, _registry, registry);
4652
4763
  }
@@ -4662,7 +4773,7 @@ var RecalcEngine = class {
4662
4773
  const s = normSheet(sheet);
4663
4774
  const r = normRef(ref);
4664
4775
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
4665
- const ast = parseFormula(text);
4776
+ const ast = parseFormulaUncached(text);
4666
4777
  const { refs, volatile } = extractReferences(
4667
4778
  ast,
4668
4779
  s,
@@ -4670,18 +4781,21 @@ var RecalcEngine = class {
4670
4781
  __privateGet(this, _model).getDefinedName ? (n, sh) => __privateGet(this, _model).getDefinedName(n, sh) : void 0
4671
4782
  );
4672
4783
  __privateMethod(this, _RecalcEngine_instances, rememberSheet_fn).call(this, sheet);
4673
- __privateGet(this, _formulas).set(keyOf(s, r), {
4784
+ const key = keyOf(s, r);
4785
+ __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, {
4674
4786
  sheet: s,
4675
4787
  sheetName: sheet,
4676
4788
  ref: r,
4789
+ key,
4677
4790
  row: row2,
4678
4791
  col: column2,
4679
4792
  formula: text,
4793
+ ast,
4680
4794
  refs,
4681
4795
  volatile
4682
4796
  });
4683
4797
  __privateGet(this, _model).setCellFormula?.(sheet, r, text);
4684
- return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }], keyOf(s, r));
4798
+ return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }], key);
4685
4799
  }
4686
4800
  /**
4687
4801
  * Removes a formula from a cell and recalculates its dependents (which may now
@@ -4690,8 +4804,9 @@ var RecalcEngine = class {
4690
4804
  clearCellFormula(sheet, ref) {
4691
4805
  const s = normSheet(sheet);
4692
4806
  const r = normRef(ref);
4693
- const existed = __privateGet(this, _formulas).delete(keyOf(s, r));
4694
- if (!existed) return [];
4807
+ const key = keyOf(s, r);
4808
+ if (!__privateGet(this, _formulas).has(key)) return [];
4809
+ __privateMethod(this, _RecalcEngine_instances, unregister_fn).call(this, key);
4695
4810
  __privateGet(this, _model).clearCell?.(sheet, r);
4696
4811
  const { row: row2, column: column2 } = puredocsExcel.parseCellRef(r);
4697
4812
  return __privateMethod(this, _RecalcEngine_instances, recalcFrom_fn).call(this, [{ sheet: s, row: row2, col: column2 }]);
@@ -4727,6 +4842,39 @@ var RecalcEngine = class {
4727
4842
  getPrecedents(sheet, ref) {
4728
4843
  return __privateGet(this, _formulas).get(keyOf(sheet, ref))?.refs ?? [];
4729
4844
  }
4845
+ // ── Structural edits ─────────────────────────────────────────────────────────
4846
+ /**
4847
+ * Tells the engine rows were inserted on a sheet. Call AFTER the
4848
+ * corresponding `Worksheet.insertRows` (the model must already reflect the
4849
+ * edit). The dependency graph is translated in place — registrations,
4850
+ * formula text and spill bookkeeping all move — and only volatile formulas
4851
+ * are recomputed, because a pure translation changes no values.
4852
+ *
4853
+ * @example
4854
+ * ws.insertRows(3, 2);
4855
+ * engine.onRowsInserted('S1', 3, 2);
4856
+ */
4857
+ onRowsInserted(sheet, at, count2 = 1) {
4858
+ return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "row", kind: "insert", at, count: count2, sheetName: sheet });
4859
+ }
4860
+ /**
4861
+ * Tells the engine rows were deleted on a sheet. Call AFTER the
4862
+ * corresponding `Worksheet.deleteRows`. Registrations inside the deleted
4863
+ * band are dropped; the rest of the graph is translated, and only formulas
4864
+ * whose precedents touched the deleted band (plus their dependents and
4865
+ * volatiles) are recomputed.
4866
+ */
4867
+ onRowsDeleted(sheet, at, count2 = 1) {
4868
+ return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "row", kind: "delete", at, count: count2, sheetName: sheet });
4869
+ }
4870
+ /** Column counterpart to {@link onRowsInserted}. */
4871
+ onColumnsInserted(sheet, at, count2 = 1) {
4872
+ return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "col", kind: "insert", at, count: count2, sheetName: sheet });
4873
+ }
4874
+ /** Column counterpart to {@link onRowsDeleted}. */
4875
+ onColumnsDeleted(sheet, at, count2 = 1) {
4876
+ return __privateMethod(this, _RecalcEngine_instances, applyShift_fn).call(this, { dim: "col", kind: "delete", at, count: count2, sheetName: sheet });
4877
+ }
4730
4878
  };
4731
4879
  _model = new WeakMap();
4732
4880
  _registry = new WeakMap();
@@ -4734,10 +4882,152 @@ _formulas = new WeakMap();
4734
4882
  _sheetNames = new WeakMap();
4735
4883
  _spills = new WeakMap();
4736
4884
  _spillOwner = new WeakMap();
4885
+ _dependents = new WeakMap();
4886
+ _volatiles = new WeakMap();
4887
+ _sheetLikes = new WeakMap();
4888
+ _workbookLike = new WeakMap();
4737
4889
  _RecalcEngine_instances = new WeakSet();
4890
+ /** Registers an entry in the map, the reverse index and the volatile set. */
4891
+ register_fn = function(entry) {
4892
+ __privateGet(this, _formulas).set(entry.key, entry);
4893
+ __privateGet(this, _dependents).add(entry.key, entry.refs);
4894
+ if (entry.volatile) __privateGet(this, _volatiles).add(entry.key);
4895
+ else __privateGet(this, _volatiles).delete(entry.key);
4896
+ };
4897
+ unregister_fn = function(key) {
4898
+ __privateGet(this, _formulas).delete(key);
4899
+ __privateGet(this, _dependents).remove(key);
4900
+ __privateGet(this, _volatiles).delete(key);
4901
+ };
4738
4902
  rememberSheet_fn = function(name) {
4739
4903
  __privateGet(this, _sheetNames).set(normSheet(name), name);
4740
4904
  };
4905
+ applyShift_fn = function(shift) {
4906
+ const editedSheet = normSheet(shift.sheetName);
4907
+ const isRowShift = shift.dim === "row";
4908
+ const bandEnd = shift.at + shift.count - 1;
4909
+ const rekeyed = [];
4910
+ const needsRecalc = /* @__PURE__ */ new Set();
4911
+ for (const e of __privateGet(this, _formulas).values()) {
4912
+ let row2 = e.row;
4913
+ let col = e.col;
4914
+ if (e.sheet === editedSheet) {
4915
+ const shifted = puredocsExcel.shiftIndex(isRowShift ? row2 : col, shift);
4916
+ if (shifted === null) continue;
4917
+ if (isRowShift) row2 = shifted;
4918
+ else col = shifted;
4919
+ }
4920
+ const touchedBand = shift.kind === "delete" && e.refs.some((r) => r.sheet === editedSheet && (isRowShift ? r.endRow >= shift.at && r.startRow <= bandEnd : r.endCol >= shift.at && r.startCol <= bandEnd));
4921
+ const rewritten = puredocsExcel.shiftFormulaRefs(e.formula, shift, e.sheetName);
4922
+ const moved = row2 !== e.row || col !== e.col;
4923
+ const ref = moved ? puredocsExcel.cellRefFromRowCol(row2, col) : e.ref;
4924
+ const key = moved ? keyOf(e.sheet, ref) : e.key;
4925
+ rekeyed.push({
4926
+ ...e,
4927
+ ref,
4928
+ key,
4929
+ row: row2,
4930
+ col,
4931
+ formula: rewritten.changed ? rewritten.text : e.formula,
4932
+ // A rewritten formula's cached AST no longer matches its text; it is
4933
+ // re-parsed lazily, and only if the cell is actually evaluated again.
4934
+ ast: rewritten.changed ? void 0 : e.ast,
4935
+ refs: __privateMethod(this, _RecalcEngine_instances, shiftPrecedents_fn).call(this, e.refs, shift, editedSheet)
4936
+ // Volatility is a property of which *functions* a formula calls, and
4937
+ // rewriting references cannot change that.
4938
+ });
4939
+ if (touchedBand || rewritten.hasRefError) needsRecalc.add(key);
4940
+ }
4941
+ __privateSet(this, _formulas, /* @__PURE__ */ new Map());
4942
+ __privateGet(this, _dependents).clear();
4943
+ __privateGet(this, _volatiles).clear();
4944
+ for (const e of rekeyed) __privateMethod(this, _RecalcEngine_instances, register_fn).call(this, e);
4945
+ __privateMethod(this, _RecalcEngine_instances, shiftSpillBookkeeping_fn).call(this, shift, editedSheet);
4946
+ const pending = /* @__PURE__ */ new Map();
4947
+ for (const k of needsRecalc) {
4948
+ const e = __privateGet(this, _formulas).get(k);
4949
+ if (e) pending.set(k, e);
4950
+ }
4951
+ for (const e of __privateGet(this, _formulas).values()) {
4952
+ if (e.volatile) pending.set(e.key, e);
4953
+ }
4954
+ return __privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, /* @__PURE__ */ new Set());
4955
+ };
4956
+ /**
4957
+ * Translates precedent rectangles across a structural edit.
4958
+ *
4959
+ * This is arithmetic rather than a re-parse of the rewritten formula, and the
4960
+ * two are equivalent by construction: `shiftFormulaRefs` derives the new
4961
+ * reference text by calling `shiftSpan`/`shiftIndex` on exactly these numbers,
4962
+ * so applying the same functions to the stored rectangle yields the same
4963
+ * region. A rectangle that is entirely deleted disappears, matching the
4964
+ * `#REF!` the text rewrite produces.
4965
+ */
4966
+ shiftPrecedents_fn = function(refs, shift, editedSheet) {
4967
+ const out = [];
4968
+ for (const r of refs) {
4969
+ if (r.sheet !== editedSheet) {
4970
+ out.push(r);
4971
+ continue;
4972
+ }
4973
+ if (shift.dim === "row") {
4974
+ const span = puredocsExcel.shiftSpan(r.startRow, r.endRow, shift);
4975
+ if (!span) continue;
4976
+ out.push({
4977
+ sheet: r.sheet,
4978
+ startRow: span.start,
4979
+ endRow: Math.min(span.end, puredocsExcel.EXCEL_MAX_ROWS),
4980
+ startCol: r.startCol,
4981
+ endCol: r.endCol
4982
+ });
4983
+ } else {
4984
+ const span = puredocsExcel.shiftSpan(r.startCol, r.endCol, shift);
4985
+ if (!span) continue;
4986
+ out.push({
4987
+ sheet: r.sheet,
4988
+ startRow: r.startRow,
4989
+ endRow: r.endRow,
4990
+ startCol: span.start,
4991
+ endCol: Math.min(span.end, puredocsExcel.EXCEL_MAX_COLUMNS)
4992
+ });
4993
+ }
4994
+ }
4995
+ return out;
4996
+ };
4997
+ /** Translates spill ownership across a structural edit on `editedSheet`. */
4998
+ shiftSpillBookkeeping_fn = function(shift, editedSheet) {
4999
+ const shiftCellKey = (key) => {
5000
+ const { sheet, row: row2, col } = parseKey(key);
5001
+ if (sheet !== editedSheet) return key;
5002
+ const r = shift.dim === "row" ? puredocsExcel.shiftIndex(row2, shift) : row2;
5003
+ const c = shift.dim === "col" ? puredocsExcel.shiftIndex(col, shift) : col;
5004
+ if (r === null || c === null) return null;
5005
+ return keyOf(sheet, puredocsExcel.cellRefFromRowCol(r, c));
5006
+ };
5007
+ const newSpills = /* @__PURE__ */ new Map();
5008
+ __privateGet(this, _spillOwner).clear();
5009
+ for (const [anchor, members] of __privateGet(this, _spills)) {
5010
+ const newAnchor = shiftCellKey(anchor);
5011
+ if (newAnchor === null) {
5012
+ for (const m of members) {
5013
+ const shifted = shiftCellKey(m);
5014
+ if (!shifted) continue;
5015
+ const cell = parseKey(shifted);
5016
+ __privateGet(this, _model).setCellValue(__privateGet(this, _sheetNames).get(cell.sheet) ?? cell.sheet, cell.ref, null);
5017
+ }
5018
+ continue;
5019
+ }
5020
+ const newMembers = /* @__PURE__ */ new Set();
5021
+ for (const m of members) {
5022
+ const shifted = shiftCellKey(m);
5023
+ if (!shifted) continue;
5024
+ newMembers.add(shifted);
5025
+ __privateGet(this, _spillOwner).set(shifted, newAnchor);
5026
+ }
5027
+ newSpills.set(newAnchor, newMembers);
5028
+ }
5029
+ __privateSet(this, _spills, newSpills);
5030
+ };
4741
5031
  // ── Internals ────────────────────────────────────────────────────────────────
4742
5032
  /**
4743
5033
  * Recomputes formulas affected by the given changed cells, to a fixpoint.
@@ -4753,13 +5043,17 @@ recalcFrom_fn = function(seeds, includeKey) {
4753
5043
  const results = [];
4754
5044
  const evaluated = /* @__PURE__ */ new Set();
4755
5045
  let pending = __privateMethod(this, _RecalcEngine_instances, dependentClosure_fn).call(this, seeds, evaluated);
4756
- for (const e of __privateGet(this, _formulas).values()) {
4757
- if (e.volatile) pending.set(keyOf(e.sheet, e.ref), e);
4758
- }
5046
+ __privateMethod(this, _RecalcEngine_instances, addVolatiles_fn).call(this, pending);
4759
5047
  if (includeKey) {
4760
5048
  const self = __privateGet(this, _formulas).get(includeKey);
4761
5049
  if (self) pending.set(includeKey, self);
4762
5050
  }
5051
+ results.push(...__privateMethod(this, _RecalcEngine_instances, runFixpoint_fn).call(this, pending, evaluated));
5052
+ return results;
5053
+ };
5054
+ /** Evaluates `pending` entries to a fixpoint, following spill-induced changes. */
5055
+ runFixpoint_fn = function(pending, evaluated) {
5056
+ const results = [];
4763
5057
  let guard = 0;
4764
5058
  const maxIterations = __privateGet(this, _formulas).size + 8;
4765
5059
  while (pending.size > 0 && guard++ <= maxIterations) {
@@ -4770,28 +5064,47 @@ recalcFrom_fn = function(seeds, includeKey) {
4770
5064
  }
4771
5065
  return results;
4772
5066
  };
4773
- /** Formula entries (not yet evaluated) whose precedents include one of the cells. */
5067
+ /** Adds every volatile formula to a pending set. */
5068
+ addVolatiles_fn = function(pending) {
5069
+ for (const key of __privateGet(this, _volatiles)) {
5070
+ const e = __privateGet(this, _formulas).get(key);
5071
+ if (e) pending.set(key, e);
5072
+ }
5073
+ };
5074
+ /**
5075
+ * Formula entries (not yet evaluated) whose precedents include one of the
5076
+ * cells, transitively. The queue is walked with a head index — Array.shift()
5077
+ * is O(n) per dequeue, which turns a long dependency chain quadratic.
5078
+ */
4774
5079
  dependentClosure_fn = function(cells, evaluated) {
4775
5080
  const out = /* @__PURE__ */ new Map();
4776
5081
  const queue = [...cells];
4777
- while (queue.length > 0) {
4778
- const cell = queue.shift();
4779
- for (const dep of __privateMethod(this, _RecalcEngine_instances, directDependents_fn).call(this, cell)) {
4780
- const k = keyOf(dep.sheet, dep.ref);
4781
- if (out.has(k) || evaluated.has(k)) continue;
4782
- out.set(k, dep);
5082
+ for (let head = 0; head < queue.length; head++) {
5083
+ const cell = queue[head];
5084
+ __privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, cell, (dep) => {
5085
+ if (out.has(dep.key) || evaluated.has(dep.key)) return;
5086
+ out.set(dep.key, dep);
4783
5087
  queue.push({ sheet: dep.sheet, row: dep.row, col: dep.col });
4784
- }
5088
+ });
4785
5089
  }
4786
5090
  return out;
4787
5091
  };
4788
- /** Formula entries whose precedents include the given cell. */
4789
- directDependents_fn = function(cell) {
4790
- const out = [];
4791
- for (const e of __privateGet(this, _formulas).values()) {
4792
- if (e.refs.some((ref) => refContains(ref, cell.sheet, cell.row, cell.col))) out.push(e);
4793
- }
4794
- return out;
5092
+ /**
5093
+ * Visits the formula entries whose precedents include the given cell, via the
5094
+ * reverse index. The index over-approximates (it buckets rectangles into
5095
+ * tiles), so each candidate is still confirmed against its real rectangles.
5096
+ */
5097
+ forEachDirectDependent_fn = function(cell, visit) {
5098
+ __privateGet(this, _dependents).candidates(cell.sheet, cell.row, cell.col, (key) => {
5099
+ const e = __privateGet(this, _formulas).get(key);
5100
+ if (!e) return;
5101
+ for (const ref of e.refs) {
5102
+ if (refContains(ref, cell.sheet, cell.row, cell.col)) {
5103
+ visit(e);
5104
+ return;
5105
+ }
5106
+ }
5107
+ });
4795
5108
  };
4796
5109
  /**
4797
5110
  * Topologically sorts the given entries and evaluates them, writing results
@@ -4800,32 +5113,32 @@ directDependents_fn = function(cell) {
4800
5113
  */
4801
5114
  evaluatePass_fn = function(entries) {
4802
5115
  const inSet = /* @__PURE__ */ new Map();
4803
- for (const e of entries) inSet.set(keyOf(e.sheet, e.ref), e);
5116
+ for (const e of entries) inSet.set(e.key, e);
4804
5117
  const indegree = /* @__PURE__ */ new Map();
4805
5118
  const dependents = /* @__PURE__ */ new Map();
4806
5119
  for (const k of inSet.keys()) {
4807
5120
  indegree.set(k, 0);
4808
5121
  dependents.set(k, []);
4809
5122
  }
4810
- for (const e of entries) {
4811
- const depKey = keyOf(e.sheet, e.ref);
4812
- for (const [pk, pe] of inSet) {
4813
- if (pk === depKey) continue;
4814
- if (e.refs.some((ref) => refContains(ref, pe.sheet, pe.row, pe.col))) {
4815
- dependents.get(pk).push(depKey);
4816
- indegree.set(depKey, indegree.get(depKey) + 1);
4817
- }
4818
- }
5123
+ for (const pe of entries) {
5124
+ const seen = /* @__PURE__ */ new Set();
5125
+ __privateMethod(this, _RecalcEngine_instances, forEachDirectDependent_fn).call(this, { sheet: pe.sheet, row: pe.row, col: pe.col }, (dep) => {
5126
+ if (dep.key === pe.key || !inSet.has(dep.key) || seen.has(dep.key)) return;
5127
+ seen.add(dep.key);
5128
+ dependents.get(pe.key).push(dep.key);
5129
+ indegree.set(dep.key, indegree.get(dep.key) + 1);
5130
+ });
4819
5131
  }
4820
5132
  const ready = [];
4821
5133
  for (const [k, d] of indegree) if (d === 0) ready.push(k);
4822
5134
  const order = [];
4823
- while (ready.length > 0) {
4824
- const k = ready.shift();
5135
+ for (let head = 0; head < ready.length; head++) {
5136
+ const k = ready[head];
4825
5137
  order.push(k);
4826
5138
  for (const d of dependents.get(k)) {
4827
- indegree.set(d, indegree.get(d) - 1);
4828
- if (indegree.get(d) === 0) ready.push(d);
5139
+ const left2 = indegree.get(d) - 1;
5140
+ indegree.set(d, left2);
5141
+ if (left2 === 0) ready.push(d);
4829
5142
  }
4830
5143
  }
4831
5144
  const results = [];
@@ -4839,7 +5152,7 @@ evaluatePass_fn = function(entries) {
4839
5152
  }
4840
5153
  for (const [k, e] of inSet) {
4841
5154
  if (emitted.has(k)) continue;
4842
- __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, keyOf(e.sheet, e.ref), changed);
5155
+ __privateMethod(this, _RecalcEngine_instances, clearSpill_fn).call(this, e.key, changed);
4843
5156
  const value2 = FormulaValue.errorRef.toObject();
4844
5157
  __privateGet(this, _model).setCellValue(e.sheetName, e.ref, value2);
4845
5158
  changed.push({ sheet: e.sheet, row: e.row, col: e.col });
@@ -4854,7 +5167,7 @@ evaluatePass_fn = function(entries) {
4854
5167
  * an array shrinks, previously-spilled cells it no longer covers are cleared.
4855
5168
  */
4856
5169
  writeResult_fn = function(e) {
4857
- const anchorKey = keyOf(e.sheet, e.ref);
5170
+ const anchorKey = e.key;
4858
5171
  const fv2 = __privateMethod(this, _RecalcEngine_instances, evaluateFormulaValue_fn).call(this, e);
4859
5172
  const changed = [];
4860
5173
  const prevSpill = __privateGet(this, _spills).get(anchorKey) ?? /* @__PURE__ */ new Set();
@@ -4938,15 +5251,33 @@ clearSpill_fn = function(anchorKey, changed) {
4938
5251
  __privateGet(this, _spills).delete(anchorKey);
4939
5252
  };
4940
5253
  evaluateFormulaValue_fn = function(e) {
4941
- const wb = __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this);
4942
- const ws = __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName);
4943
- return evaluateFormula(e.formula, ws, {
4944
- workbook: wb,
5254
+ if (!e.ast) {
5255
+ try {
5256
+ e.ast = parseFormulaUncached(e.formula);
5257
+ } catch {
5258
+ return FormulaValue.errorValue;
5259
+ }
5260
+ }
5261
+ return evaluateAst(e.ast, __privateMethod(this, _RecalcEngine_instances, sheetLike_fn).call(this, e.sheetName), {
5262
+ workbook: __privateMethod(this, _RecalcEngine_instances, buildWorkbookLike_fn).call(this),
4945
5263
  formulaRow: e.row,
4946
5264
  formulaCol: e.col
4947
5265
  });
4948
5266
  };
5267
+ /**
5268
+ * Per-sheet evaluator adapter. Memoised: these hold no per-formula state, and
5269
+ * rebuilding one (with its four closures) for every formula evaluated made a
5270
+ * full recalc allocate several objects per cell.
5271
+ */
4949
5272
  sheetLike_fn = function(sheetName) {
5273
+ let cached = __privateGet(this, _sheetLikes).get(sheetName);
5274
+ if (!cached) {
5275
+ cached = __privateMethod(this, _RecalcEngine_instances, buildSheetLike_fn).call(this, sheetName);
5276
+ __privateGet(this, _sheetLikes).set(sheetName, cached);
5277
+ }
5278
+ return cached;
5279
+ };
5280
+ buildSheetLike_fn = function(sheetName) {
4950
5281
  return {
4951
5282
  name: sheetName,
4952
5283
  getCellValue: (ref) => __privateGet(this, _model).getCellValue(sheetName, ref),
@@ -4956,8 +5287,9 @@ sheetLike_fn = function(sheetName) {
4956
5287
  };
4957
5288
  };
4958
5289
  buildWorkbookLike_fn = function() {
5290
+ if (__privateGet(this, _workbookLike)) return __privateGet(this, _workbookLike);
4959
5291
  const self = this;
4960
- return {
5292
+ return __privateSet(this, _workbookLike, {
4961
5293
  get worksheets() {
4962
5294
  return [...__privateGet(self, _sheetNames).values()].map((n) => {
4963
5295
  var _a;
@@ -4969,7 +5301,7 @@ buildWorkbookLike_fn = function() {
4969
5301
  return __privateMethod(_a = self, _RecalcEngine_instances, sheetLike_fn).call(_a, name);
4970
5302
  },
4971
5303
  getDefinedName: __privateGet(this, _model).getDefinedName ? (name, sheet) => __privateGet(this, _model).getDefinedName(name, sheet) : void 0
4972
- };
5304
+ });
4973
5305
  };
4974
5306
 
4975
5307
  exports.ArrayValue = ArrayValue;
@@ -5004,10 +5336,12 @@ exports.UnaryOpNode = UnaryOpNode;
5004
5336
  exports.UnaryOperator = UnaryOperator;
5005
5337
  exports.clearAstCache = clearAstCache;
5006
5338
  exports.createWorkbookRecalcModel = createWorkbookRecalcModel;
5339
+ exports.evaluateAst = evaluateAst;
5007
5340
  exports.evaluateFormula = evaluateFormula;
5008
5341
  exports.extractReferences = extractReferences;
5009
5342
  exports.getAstCacheStats = getAstCacheStats;
5010
5343
  exports.parseFormula = parseFormula;
5344
+ exports.parseFormulaUncached = parseFormulaUncached;
5011
5345
  exports.refContains = refContains;
5012
5346
  exports.registerAggregate = registerAggregate;
5013
5347
  exports.registerArray = registerArray;