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