@cj-tech-master/excelts 9.2.1 → 9.3.0

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.
Files changed (383) hide show
  1. package/README.md +25 -2
  2. package/README_zh.md +29 -6
  3. package/dist/browser/index.browser.d.ts +1 -1
  4. package/dist/browser/index.browser.js +4 -0
  5. package/dist/browser/index.d.ts +1 -1
  6. package/dist/browser/index.js +4 -0
  7. package/dist/browser/modules/excel/cell.d.ts +17 -3
  8. package/dist/browser/modules/excel/cell.js +170 -22
  9. package/dist/browser/modules/excel/defined-names.d.ts +96 -1
  10. package/dist/browser/modules/excel/defined-names.js +411 -21
  11. package/dist/browser/modules/excel/image.d.ts +11 -0
  12. package/dist/browser/modules/excel/image.js +24 -1
  13. package/dist/browser/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
  14. package/dist/browser/modules/excel/stream/workbook-reader.browser.js +14 -0
  15. package/dist/browser/modules/excel/stream/workbook-reader.d.ts +2 -1
  16. package/dist/browser/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
  17. package/dist/browser/modules/excel/stream/workbook-writer.browser.js +48 -1
  18. package/dist/browser/modules/excel/stream/workbook-writer.d.ts +3 -2
  19. package/dist/browser/modules/excel/stream/worksheet-reader.js +17 -1
  20. package/dist/browser/modules/excel/stream/worksheet-writer.d.ts +39 -6
  21. package/dist/browser/modules/excel/stream/worksheet-writer.js +45 -5
  22. package/dist/browser/modules/excel/table.js +15 -2
  23. package/dist/browser/modules/excel/types.d.ts +133 -2
  24. package/dist/browser/modules/excel/utils/col-cache.d.ts +1 -0
  25. package/dist/browser/modules/excel/utils/col-cache.js +15 -0
  26. package/dist/browser/modules/excel/utils/drawing-utils.d.ts +3 -3
  27. package/dist/browser/modules/excel/utils/drawing-utils.js +4 -0
  28. package/dist/browser/modules/excel/utils/external-link-formula.d.ts +76 -0
  29. package/dist/browser/modules/excel/utils/external-link-formula.js +208 -0
  30. package/dist/browser/modules/excel/utils/iterate-stream.d.ts +9 -3
  31. package/dist/browser/modules/excel/utils/iterate-stream.js +3 -1
  32. package/dist/browser/modules/excel/utils/ooxml-paths.d.ts +19 -0
  33. package/dist/browser/modules/excel/utils/ooxml-paths.js +37 -2
  34. package/dist/browser/modules/excel/utils/shared-strings.d.ts +8 -3
  35. package/dist/browser/modules/excel/utils/shared-strings.js +21 -2
  36. package/dist/browser/modules/excel/utils/workbook-protection.d.ts +30 -0
  37. package/dist/browser/modules/excel/utils/workbook-protection.js +30 -0
  38. package/dist/browser/modules/excel/workbook.browser.d.ts +257 -6
  39. package/dist/browser/modules/excel/workbook.browser.js +318 -34
  40. package/dist/browser/modules/excel/workbook.d.ts +1 -1
  41. package/dist/browser/modules/excel/worksheet.d.ts +3 -1
  42. package/dist/browser/modules/excel/worksheet.js +21 -2
  43. package/dist/browser/modules/excel/xlsx/rel-type.d.ts +15 -0
  44. package/dist/browser/modules/excel/xlsx/rel-type.js +16 -1
  45. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
  46. package/dist/browser/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  47. package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
  48. package/dist/browser/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
  49. package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
  50. package/dist/browser/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
  51. package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
  52. package/dist/browser/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  53. package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
  54. package/dist/browser/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
  55. package/dist/browser/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  56. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
  57. package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
  58. package/dist/browser/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
  59. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
  60. package/dist/browser/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
  61. package/dist/browser/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  62. package/dist/browser/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  63. package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
  64. package/dist/browser/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  65. package/dist/browser/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  66. package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
  67. package/dist/browser/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
  68. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  69. package/dist/browser/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  70. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
  71. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +410 -20
  72. package/dist/browser/modules/excel/xlsx/xlsx.d.ts +7 -4
  73. package/dist/browser/modules/excel/xlsx/xlsx.js +4 -5
  74. package/dist/browser/modules/formula/compile/address-utils.d.ts +62 -0
  75. package/dist/browser/modules/formula/compile/address-utils.js +83 -0
  76. package/dist/browser/modules/formula/compile/binder.d.ts +42 -0
  77. package/dist/browser/modules/formula/compile/binder.js +487 -0
  78. package/dist/browser/modules/formula/compile/bound-ast.d.ts +230 -0
  79. package/dist/browser/modules/formula/compile/bound-ast.js +80 -0
  80. package/dist/browser/modules/formula/compile/compiled-formula.d.ts +137 -0
  81. package/dist/browser/modules/formula/compile/compiled-formula.js +383 -0
  82. package/dist/browser/modules/formula/compile/dependency-analysis.d.ts +93 -0
  83. package/dist/browser/modules/formula/compile/dependency-analysis.js +432 -0
  84. package/dist/browser/modules/formula/compile/structured-ref-utils.d.ts +93 -0
  85. package/dist/browser/modules/formula/compile/structured-ref-utils.js +136 -0
  86. package/dist/browser/modules/formula/default-syntax-probe.d.ts +79 -0
  87. package/dist/browser/modules/formula/default-syntax-probe.js +83 -0
  88. package/dist/browser/modules/formula/functions/_date-context.d.ts +4 -0
  89. package/dist/browser/modules/formula/functions/_date-context.js +29 -0
  90. package/dist/browser/modules/formula/functions/_shared.d.ts +121 -0
  91. package/dist/browser/modules/formula/functions/_shared.js +381 -0
  92. package/dist/browser/modules/formula/functions/conditional.d.ts +27 -0
  93. package/dist/browser/modules/formula/functions/conditional.js +343 -0
  94. package/dist/browser/modules/formula/functions/database.d.ts +37 -0
  95. package/dist/browser/modules/formula/functions/database.js +274 -0
  96. package/dist/browser/modules/formula/functions/date.d.ts +61 -0
  97. package/dist/browser/modules/formula/functions/date.js +855 -0
  98. package/dist/browser/modules/formula/functions/dynamic-array.d.ts +23 -0
  99. package/dist/browser/modules/formula/functions/dynamic-array.js +860 -0
  100. package/dist/browser/modules/formula/functions/engineering.d.ts +57 -0
  101. package/dist/browser/modules/formula/functions/engineering.js +1128 -0
  102. package/dist/browser/modules/formula/functions/financial.d.ts +202 -0
  103. package/dist/browser/modules/formula/functions/financial.js +2296 -0
  104. package/dist/browser/modules/formula/functions/lookup.d.ts +18 -0
  105. package/dist/browser/modules/formula/functions/lookup.js +886 -0
  106. package/dist/browser/modules/formula/functions/math.d.ts +114 -0
  107. package/dist/browser/modules/formula/functions/math.js +1406 -0
  108. package/dist/browser/modules/formula/functions/statistical.d.ts +193 -0
  109. package/dist/browser/modules/formula/functions/statistical.js +3390 -0
  110. package/dist/browser/modules/formula/functions/text.d.ts +86 -0
  111. package/dist/browser/modules/formula/functions/text.js +1845 -0
  112. package/dist/browser/modules/formula/host-registry.d.ts +53 -0
  113. package/dist/browser/modules/formula/host-registry.js +69 -0
  114. package/dist/browser/modules/formula/index.d.ts +39 -0
  115. package/dist/browser/modules/formula/index.js +49 -0
  116. package/dist/browser/modules/formula/install.d.ts +62 -0
  117. package/dist/browser/modules/formula/install.js +88 -0
  118. package/dist/browser/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
  119. package/dist/browser/modules/formula/integration/apply-writeback-plan.js +210 -0
  120. package/dist/browser/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
  121. package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +616 -0
  122. package/dist/browser/modules/formula/integration/calculate-formulas.d.ts +67 -0
  123. package/dist/browser/modules/formula/integration/calculate-formulas.js +68 -0
  124. package/dist/browser/modules/formula/integration/formula-instance.d.ts +64 -0
  125. package/dist/browser/modules/formula/integration/formula-instance.js +79 -0
  126. package/dist/browser/modules/formula/integration/workbook-adapter.d.ts +26 -0
  127. package/dist/browser/modules/formula/integration/workbook-adapter.js +324 -0
  128. package/dist/browser/modules/formula/integration/workbook-snapshot.d.ts +267 -0
  129. package/dist/browser/modules/formula/integration/workbook-snapshot.js +77 -0
  130. package/dist/browser/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
  131. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +473 -0
  132. package/dist/browser/modules/formula/materialize/spill-engine.d.ts +9 -0
  133. package/dist/browser/modules/formula/materialize/spill-engine.js +38 -0
  134. package/dist/browser/modules/formula/materialize/types.d.ts +179 -0
  135. package/dist/browser/modules/formula/materialize/types.js +29 -0
  136. package/dist/browser/modules/formula/materialize/writeback-plan.d.ts +167 -0
  137. package/dist/browser/modules/formula/materialize/writeback-plan.js +27 -0
  138. package/dist/browser/modules/formula/runtime/evaluator.d.ts +151 -0
  139. package/dist/browser/modules/formula/runtime/evaluator.js +2291 -0
  140. package/dist/browser/modules/formula/runtime/function-registry.d.ts +47 -0
  141. package/dist/browser/modules/formula/runtime/function-registry.js +840 -0
  142. package/dist/browser/modules/formula/runtime/values.d.ts +211 -0
  143. package/dist/browser/modules/formula/runtime/values.js +385 -0
  144. package/dist/browser/modules/formula/syntax/ast.d.ts +129 -0
  145. package/dist/browser/modules/formula/syntax/ast.js +28 -0
  146. package/dist/browser/modules/formula/syntax/parser.d.ts +18 -0
  147. package/dist/browser/modules/formula/syntax/parser.js +439 -0
  148. package/dist/browser/modules/formula/syntax/token-types.d.ts +153 -0
  149. package/dist/browser/modules/formula/syntax/token-types.js +59 -0
  150. package/dist/browser/modules/formula/syntax/tokenizer.d.ts +10 -0
  151. package/dist/browser/modules/formula/syntax/tokenizer.js +1074 -0
  152. package/dist/browser/modules/pdf/excel-bridge.js +9 -0
  153. package/dist/cjs/index.js +4 -0
  154. package/dist/cjs/modules/excel/cell.js +170 -22
  155. package/dist/cjs/modules/excel/defined-names.js +411 -21
  156. package/dist/cjs/modules/excel/image.js +24 -1
  157. package/dist/cjs/modules/excel/stream/workbook-reader.browser.js +14 -0
  158. package/dist/cjs/modules/excel/stream/workbook-writer.browser.js +48 -1
  159. package/dist/cjs/modules/excel/stream/worksheet-reader.js +17 -1
  160. package/dist/cjs/modules/excel/stream/worksheet-writer.js +45 -5
  161. package/dist/cjs/modules/excel/table.js +15 -2
  162. package/dist/cjs/modules/excel/utils/col-cache.js +15 -0
  163. package/dist/cjs/modules/excel/utils/drawing-utils.js +4 -0
  164. package/dist/cjs/modules/excel/utils/external-link-formula.js +212 -0
  165. package/dist/cjs/modules/excel/utils/iterate-stream.js +3 -1
  166. package/dist/cjs/modules/excel/utils/ooxml-paths.js +42 -2
  167. package/dist/cjs/modules/excel/utils/shared-strings.js +21 -2
  168. package/dist/cjs/modules/excel/utils/workbook-protection.js +33 -0
  169. package/dist/cjs/modules/excel/workbook.browser.js +318 -34
  170. package/dist/cjs/modules/excel/worksheet.js +20 -1
  171. package/dist/cjs/modules/excel/xlsx/rel-type.js +16 -1
  172. package/dist/cjs/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  173. package/dist/cjs/modules/excel/xlsx/xform/book/external-link-xform.js +333 -0
  174. package/dist/cjs/modules/excel/xlsx/xform/book/external-reference-xform.js +27 -0
  175. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  176. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-protection-xform.js +69 -0
  177. package/dist/cjs/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  178. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +18 -0
  179. package/dist/cjs/modules/excel/xlsx/xform/core/metadata-xform.js +161 -0
  180. package/dist/cjs/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +108 -0
  181. package/dist/cjs/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  182. package/dist/cjs/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  183. package/dist/cjs/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  184. package/dist/cjs/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  185. package/dist/cjs/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +83 -0
  186. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  187. package/dist/cjs/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  188. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +408 -18
  189. package/dist/cjs/modules/excel/xlsx/xlsx.js +4 -5
  190. package/dist/cjs/modules/formula/compile/address-utils.js +89 -0
  191. package/dist/cjs/modules/formula/compile/binder.js +489 -0
  192. package/dist/cjs/modules/formula/compile/bound-ast.js +68 -0
  193. package/dist/cjs/modules/formula/compile/compiled-formula.js +387 -0
  194. package/dist/cjs/modules/formula/compile/dependency-analysis.js +437 -0
  195. package/dist/cjs/modules/formula/compile/structured-ref-utils.js +141 -0
  196. package/dist/cjs/modules/formula/default-syntax-probe.js +87 -0
  197. package/dist/cjs/modules/formula/functions/_date-context.js +33 -0
  198. package/dist/cjs/modules/formula/functions/_shared.js +396 -0
  199. package/dist/cjs/modules/formula/functions/conditional.js +354 -0
  200. package/dist/cjs/modules/formula/functions/database.js +288 -0
  201. package/dist/cjs/modules/formula/functions/date.js +883 -0
  202. package/dist/cjs/modules/formula/functions/dynamic-array.js +881 -0
  203. package/dist/cjs/modules/formula/functions/engineering.js +1183 -0
  204. package/dist/cjs/modules/formula/functions/financial.js +2348 -0
  205. package/dist/cjs/modules/formula/functions/lookup.js +902 -0
  206. package/dist/cjs/modules/formula/functions/math.js +1487 -0
  207. package/dist/cjs/modules/formula/functions/statistical.js +3488 -0
  208. package/dist/cjs/modules/formula/functions/text.js +1889 -0
  209. package/dist/cjs/modules/formula/host-registry.js +75 -0
  210. package/dist/cjs/modules/formula/index.js +58 -0
  211. package/dist/cjs/modules/formula/install.js +93 -0
  212. package/dist/cjs/modules/formula/integration/apply-writeback-plan.js +213 -0
  213. package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +619 -0
  214. package/dist/cjs/modules/formula/integration/calculate-formulas.js +71 -0
  215. package/dist/cjs/modules/formula/integration/formula-instance.js +82 -0
  216. package/dist/cjs/modules/formula/integration/workbook-adapter.js +327 -0
  217. package/dist/cjs/modules/formula/integration/workbook-snapshot.js +84 -0
  218. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +475 -0
  219. package/dist/cjs/modules/formula/materialize/spill-engine.js +42 -0
  220. package/dist/cjs/modules/formula/materialize/types.js +32 -0
  221. package/dist/cjs/modules/formula/materialize/writeback-plan.js +28 -0
  222. package/dist/cjs/modules/formula/runtime/evaluator.js +2298 -0
  223. package/dist/cjs/modules/formula/runtime/function-registry.js +846 -0
  224. package/dist/cjs/modules/formula/runtime/values.js +385 -0
  225. package/dist/cjs/modules/formula/syntax/ast.js +8 -0
  226. package/dist/cjs/modules/formula/syntax/parser.js +440 -0
  227. package/dist/cjs/modules/formula/syntax/token-types.js +32 -0
  228. package/dist/cjs/modules/formula/syntax/tokenizer.js +1076 -0
  229. package/dist/cjs/modules/pdf/excel-bridge.js +9 -0
  230. package/dist/esm/index.browser.js +4 -0
  231. package/dist/esm/index.js +4 -0
  232. package/dist/esm/modules/excel/cell.js +170 -22
  233. package/dist/esm/modules/excel/defined-names.js +411 -21
  234. package/dist/esm/modules/excel/image.js +24 -1
  235. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +14 -0
  236. package/dist/esm/modules/excel/stream/workbook-writer.browser.js +48 -1
  237. package/dist/esm/modules/excel/stream/worksheet-reader.js +17 -1
  238. package/dist/esm/modules/excel/stream/worksheet-writer.js +45 -5
  239. package/dist/esm/modules/excel/table.js +15 -2
  240. package/dist/esm/modules/excel/utils/col-cache.js +15 -0
  241. package/dist/esm/modules/excel/utils/drawing-utils.js +4 -0
  242. package/dist/esm/modules/excel/utils/external-link-formula.js +208 -0
  243. package/dist/esm/modules/excel/utils/iterate-stream.js +3 -1
  244. package/dist/esm/modules/excel/utils/ooxml-paths.js +37 -2
  245. package/dist/esm/modules/excel/utils/shared-strings.js +21 -2
  246. package/dist/esm/modules/excel/utils/workbook-protection.js +30 -0
  247. package/dist/esm/modules/excel/workbook.browser.js +318 -34
  248. package/dist/esm/modules/excel/worksheet.js +21 -2
  249. package/dist/esm/modules/excel/xlsx/rel-type.js +16 -1
  250. package/dist/esm/modules/excel/xlsx/xform/book/defined-name-xform.js +21 -86
  251. package/dist/esm/modules/excel/xlsx/xform/book/external-link-xform.js +330 -0
  252. package/dist/esm/modules/excel/xlsx/xform/book/external-reference-xform.js +24 -0
  253. package/dist/esm/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.js +11 -2
  254. package/dist/esm/modules/excel/xlsx/xform/book/workbook-protection-xform.js +66 -0
  255. package/dist/esm/modules/excel/xlsx/xform/book/workbook-xform.js +38 -5
  256. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +19 -1
  257. package/dist/esm/modules/excel/xlsx/xform/core/metadata-xform.js +158 -0
  258. package/dist/esm/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.js +105 -0
  259. package/dist/esm/modules/excel/xlsx/xform/drawing/base-cell-anchor-xform.js +3 -0
  260. package/dist/esm/modules/excel/xlsx/xform/drawing/drawing-xform.js +10 -2
  261. package/dist/esm/modules/excel/xlsx/xform/sheet/cell-xform.js +166 -8
  262. package/dist/esm/modules/excel/xlsx/xform/sheet/data-validations-xform.js +1 -1
  263. package/dist/esm/modules/excel/xlsx/xform/sheet/ignored-errors-xform.js +80 -0
  264. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +9 -4
  265. package/dist/esm/modules/excel/xlsx/xform/style/border-xform.js +4 -1
  266. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +410 -20
  267. package/dist/esm/modules/excel/xlsx/xlsx.js +4 -5
  268. package/dist/esm/modules/formula/compile/address-utils.js +83 -0
  269. package/dist/esm/modules/formula/compile/binder.js +487 -0
  270. package/dist/esm/modules/formula/compile/bound-ast.js +80 -0
  271. package/dist/esm/modules/formula/compile/compiled-formula.js +383 -0
  272. package/dist/esm/modules/formula/compile/dependency-analysis.js +432 -0
  273. package/dist/esm/modules/formula/compile/structured-ref-utils.js +136 -0
  274. package/dist/esm/modules/formula/default-syntax-probe.js +83 -0
  275. package/dist/esm/modules/formula/functions/_date-context.js +29 -0
  276. package/dist/esm/modules/formula/functions/_shared.js +381 -0
  277. package/dist/esm/modules/formula/functions/conditional.js +343 -0
  278. package/dist/esm/modules/formula/functions/database.js +274 -0
  279. package/dist/esm/modules/formula/functions/date.js +855 -0
  280. package/dist/esm/modules/formula/functions/dynamic-array.js +860 -0
  281. package/dist/esm/modules/formula/functions/engineering.js +1128 -0
  282. package/dist/esm/modules/formula/functions/financial.js +2296 -0
  283. package/dist/esm/modules/formula/functions/lookup.js +886 -0
  284. package/dist/esm/modules/formula/functions/math.js +1406 -0
  285. package/dist/esm/modules/formula/functions/statistical.js +3390 -0
  286. package/dist/esm/modules/formula/functions/text.js +1845 -0
  287. package/dist/esm/modules/formula/host-registry.js +69 -0
  288. package/dist/esm/modules/formula/index.js +49 -0
  289. package/dist/esm/modules/formula/install.js +88 -0
  290. package/dist/esm/modules/formula/integration/apply-writeback-plan.js +210 -0
  291. package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +616 -0
  292. package/dist/esm/modules/formula/integration/calculate-formulas.js +68 -0
  293. package/dist/esm/modules/formula/integration/formula-instance.js +79 -0
  294. package/dist/esm/modules/formula/integration/workbook-adapter.js +324 -0
  295. package/dist/esm/modules/formula/integration/workbook-snapshot.js +77 -0
  296. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +473 -0
  297. package/dist/esm/modules/formula/materialize/spill-engine.js +38 -0
  298. package/dist/esm/modules/formula/materialize/types.js +29 -0
  299. package/dist/esm/modules/formula/materialize/writeback-plan.js +27 -0
  300. package/dist/esm/modules/formula/runtime/evaluator.js +2291 -0
  301. package/dist/esm/modules/formula/runtime/function-registry.js +840 -0
  302. package/dist/esm/modules/formula/runtime/values.js +385 -0
  303. package/dist/esm/modules/formula/syntax/ast.js +28 -0
  304. package/dist/esm/modules/formula/syntax/parser.js +439 -0
  305. package/dist/esm/modules/formula/syntax/token-types.js +59 -0
  306. package/dist/esm/modules/formula/syntax/tokenizer.js +1074 -0
  307. package/dist/esm/modules/pdf/excel-bridge.js +9 -0
  308. package/dist/iife/excelts.iife.js +2302 -373
  309. package/dist/iife/excelts.iife.js.map +1 -1
  310. package/dist/iife/excelts.iife.min.js +34 -34
  311. package/dist/types/index.browser.d.ts +1 -1
  312. package/dist/types/index.d.ts +1 -1
  313. package/dist/types/modules/excel/cell.d.ts +17 -3
  314. package/dist/types/modules/excel/defined-names.d.ts +96 -1
  315. package/dist/types/modules/excel/image.d.ts +11 -0
  316. package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +9 -3
  317. package/dist/types/modules/excel/stream/workbook-reader.d.ts +2 -1
  318. package/dist/types/modules/excel/stream/workbook-writer.browser.d.ts +39 -5
  319. package/dist/types/modules/excel/stream/workbook-writer.d.ts +3 -2
  320. package/dist/types/modules/excel/stream/worksheet-writer.d.ts +39 -6
  321. package/dist/types/modules/excel/types.d.ts +133 -2
  322. package/dist/types/modules/excel/utils/col-cache.d.ts +1 -0
  323. package/dist/types/modules/excel/utils/drawing-utils.d.ts +3 -3
  324. package/dist/types/modules/excel/utils/external-link-formula.d.ts +76 -0
  325. package/dist/types/modules/excel/utils/iterate-stream.d.ts +9 -3
  326. package/dist/types/modules/excel/utils/ooxml-paths.d.ts +19 -0
  327. package/dist/types/modules/excel/utils/shared-strings.d.ts +8 -3
  328. package/dist/types/modules/excel/utils/workbook-protection.d.ts +30 -0
  329. package/dist/types/modules/excel/workbook.browser.d.ts +257 -6
  330. package/dist/types/modules/excel/workbook.d.ts +1 -1
  331. package/dist/types/modules/excel/worksheet.d.ts +3 -1
  332. package/dist/types/modules/excel/xlsx/rel-type.d.ts +15 -0
  333. package/dist/types/modules/excel/xlsx/xform/book/defined-name-xform.d.ts +6 -5
  334. package/dist/types/modules/excel/xlsx/xform/book/external-link-xform.d.ts +84 -0
  335. package/dist/types/modules/excel/xlsx/xform/book/external-reference-xform.d.ts +17 -0
  336. package/dist/types/modules/excel/xlsx/xform/book/workbook-calc-properties-xform.d.ts +3 -0
  337. package/dist/types/modules/excel/xlsx/xform/book/workbook-protection-xform.d.ts +20 -0
  338. package/dist/types/modules/excel/xlsx/xform/core/metadata-xform.d.ts +56 -0
  339. package/dist/types/modules/excel/xlsx/xform/drawing/absolute-anchor-xform.d.ts +26 -0
  340. package/dist/types/modules/excel/xlsx/xform/sheet/cell-xform.d.ts +1 -1
  341. package/dist/types/modules/excel/xlsx/xform/sheet/ignored-errors-xform.d.ts +21 -0
  342. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +172 -13
  343. package/dist/types/modules/excel/xlsx/xlsx.d.ts +7 -4
  344. package/dist/types/modules/formula/compile/address-utils.d.ts +62 -0
  345. package/dist/types/modules/formula/compile/binder.d.ts +42 -0
  346. package/dist/types/modules/formula/compile/bound-ast.d.ts +230 -0
  347. package/dist/types/modules/formula/compile/compiled-formula.d.ts +137 -0
  348. package/dist/types/modules/formula/compile/dependency-analysis.d.ts +93 -0
  349. package/dist/types/modules/formula/compile/structured-ref-utils.d.ts +93 -0
  350. package/dist/types/modules/formula/default-syntax-probe.d.ts +79 -0
  351. package/dist/types/modules/formula/functions/_date-context.d.ts +4 -0
  352. package/dist/types/modules/formula/functions/_shared.d.ts +121 -0
  353. package/dist/types/modules/formula/functions/conditional.d.ts +27 -0
  354. package/dist/types/modules/formula/functions/database.d.ts +37 -0
  355. package/dist/types/modules/formula/functions/date.d.ts +61 -0
  356. package/dist/types/modules/formula/functions/dynamic-array.d.ts +23 -0
  357. package/dist/types/modules/formula/functions/engineering.d.ts +57 -0
  358. package/dist/types/modules/formula/functions/financial.d.ts +202 -0
  359. package/dist/types/modules/formula/functions/lookup.d.ts +18 -0
  360. package/dist/types/modules/formula/functions/math.d.ts +114 -0
  361. package/dist/types/modules/formula/functions/statistical.d.ts +193 -0
  362. package/dist/types/modules/formula/functions/text.d.ts +86 -0
  363. package/dist/types/modules/formula/host-registry.d.ts +53 -0
  364. package/dist/types/modules/formula/index.d.ts +39 -0
  365. package/dist/types/modules/formula/install.d.ts +62 -0
  366. package/dist/types/modules/formula/integration/apply-writeback-plan.d.ts +26 -0
  367. package/dist/types/modules/formula/integration/calculate-formulas-impl.d.ts +30 -0
  368. package/dist/types/modules/formula/integration/calculate-formulas.d.ts +67 -0
  369. package/dist/types/modules/formula/integration/formula-instance.d.ts +64 -0
  370. package/dist/types/modules/formula/integration/workbook-adapter.d.ts +26 -0
  371. package/dist/types/modules/formula/integration/workbook-snapshot.d.ts +267 -0
  372. package/dist/types/modules/formula/materialize/build-writeback-plan.d.ts +34 -0
  373. package/dist/types/modules/formula/materialize/spill-engine.d.ts +9 -0
  374. package/dist/types/modules/formula/materialize/types.d.ts +179 -0
  375. package/dist/types/modules/formula/materialize/writeback-plan.d.ts +167 -0
  376. package/dist/types/modules/formula/runtime/evaluator.d.ts +151 -0
  377. package/dist/types/modules/formula/runtime/function-registry.d.ts +47 -0
  378. package/dist/types/modules/formula/runtime/values.d.ts +211 -0
  379. package/dist/types/modules/formula/syntax/ast.d.ts +129 -0
  380. package/dist/types/modules/formula/syntax/parser.d.ts +18 -0
  381. package/dist/types/modules/formula/syntax/token-types.d.ts +153 -0
  382. package/dist/types/modules/formula/syntax/tokenizer.d.ts +10 -0
  383. package/package.json +28 -28
@@ -0,0 +1,2291 @@
1
+ /**
2
+ * Evaluator — Execute BoundExpr using the RuntimeValue system.
3
+ *
4
+ * The evaluator operates on BoundExpr (from the compile phase),
5
+ * WorkbookSnapshot (from the snapshot phase), and RuntimeValue
6
+ * (the value system).
7
+ */
8
+ import { parseDefinedNameRange } from "../compile/address-utils.js";
9
+ import { bind } from "../compile/binder.js";
10
+ import { BoundExprKind } from "../compile/bound-ast.js";
11
+ import { resolveStructuredRefRows, buildTableGeometry, resolveStructuredRefColumns } from "../compile/structured-ref-utils.js";
12
+ import { snapshotCellKey, formulaCellKey, resolveDefinedName as resolveDefinedNameFromSnapshot } from "../integration/workbook-snapshot.js";
13
+ import { parse } from "../syntax/parser.js";
14
+ import { stripFunctionPrefix } from "../syntax/token-types.js";
15
+ import { tokenize } from "../syntax/tokenizer.js";
16
+ import { lookupFunction } from "./function-registry.js";
17
+ import { RVKind, BLANK, ERRORS, rvNumber, rvString, rvBoolean, rvError, rvArray, rvRef, rvCellRef, rvLambda, isError, isScalar, isLambda, toNumberRV, toStringRV, toBooleanRV, topLeft, scalarEquals, compareScalarsSameKind, fromSnapshotValue } from "./values.js";
18
+ /**
19
+ * Per-calculation mutable state.
20
+ */
21
+ export class EvalSession {
22
+ constructor() {
23
+ /** Cells currently on the evaluation call stack (circular-ref detection). */
24
+ this.evaluating = new Set();
25
+ /**
26
+ * Unified formula result cache.
27
+ * Each entry holds both the scalar form (for dependents) and the raw form
28
+ * (for materialize). This replaces the previous separate cache/rawCache
29
+ * pattern with a single, self-documenting structure.
30
+ */
31
+ this.resultCache = new Map();
32
+ /** Cache for runtime name resolution (defined names that need parsing). */
33
+ this.nameCache = new Map();
34
+ /** Fallback values for circular references during iterative calculation. */
35
+ this.circularFallback = new Map();
36
+ /**
37
+ * Live spill map: cell key → (masterKey, row-offset, col-offset).
38
+ *
39
+ * Populated as soon as a dynamic-array formula is evaluated and yields
40
+ * an array result. Downstream formulas that read a cell inside the
41
+ * spill region look the master's cached array up via this map and
42
+ * return the correct element — even before materialize has written
43
+ * the ghost cells to the snapshot.
44
+ *
45
+ * This is the fix for "first-pass `=SUM(A1:A5)` over a `=SEQUENCE(5)`
46
+ * spill" — without the live map, `getCellValue("S", 2, 1)` returned
47
+ * BLANK and SUM only counted the master cell.
48
+ */
49
+ this.liveSpills = new Map();
50
+ /**
51
+ * Runtime dependency recorder — tracks cell accesses made during evaluation.
52
+ *
53
+ * When a formula with `hasDynamicRefs` (INDIRECT/OFFSET) is being evaluated,
54
+ * every `getCellValue` / `buildRangeArray` call records the accessed cell/range
55
+ * key here. After evaluation, these dynamic edges can be merged with the
56
+ * compiled static dependency set to produce a complete dependency graph.
57
+ *
58
+ * Key: formula cell key being evaluated → Set of accessed cell keys.
59
+ * Only populated for formulas that have `hasDynamicRefs === true`.
60
+ */
61
+ this.dynamicDeps = new Map();
62
+ /**
63
+ * The formula cell key currently being recorded (null if recording is off).
64
+ * Set before evaluating a formula with dynamic refs, cleared after.
65
+ */
66
+ this.recordingKey = null;
67
+ /**
68
+ * Current LAMBDA invocation depth. Guards against unbounded recursion
69
+ * (e.g. `LAMBDA(x, x(x))(LAMBDA(x, x(x)))`) that would otherwise overflow
70
+ * the JS call stack. Excel documents a recursion limit of ~256.
71
+ */
72
+ this.lambdaDepth = 0;
73
+ /**
74
+ * AST cache for INDIRECT re-parsing. INDIRECT receives a runtime string
75
+ * describing a reference; re-parsing it per invocation would be wasted
76
+ * work, so we memoise the `bound` expression keyed on the reference
77
+ * text. This belongs to the session (per-calculation lifetime) rather
78
+ * than the snapshot because the bindings depend on the evaluation
79
+ * context.
80
+ */
81
+ this.indirectAstCache = new Map();
82
+ }
83
+ makeKey(sheet, row, col) {
84
+ return formulaCellKey(sheet, row, col);
85
+ }
86
+ /**
87
+ * Record a cell access for the currently-recording formula.
88
+ */
89
+ recordAccess(sheet, row, col) {
90
+ if (this.recordingKey === null) {
91
+ return;
92
+ }
93
+ let deps = this.dynamicDeps.get(this.recordingKey);
94
+ if (!deps) {
95
+ deps = new Set();
96
+ this.dynamicDeps.set(this.recordingKey, deps);
97
+ }
98
+ deps.add(formulaCellKey(sheet, row, col));
99
+ }
100
+ }
101
+ // ============================================================================
102
+ // Main Evaluate Function
103
+ // ============================================================================
104
+ /**
105
+ * Exhaustiveness helper — TypeScript narrows `never` here to prove that
106
+ * every discriminated-union variant was handled. At runtime this should be
107
+ * unreachable; if a new variant is added without a case, compilation fails.
108
+ */
109
+ function assertNever(x) {
110
+ throw new Error(`unexpected variant: ${JSON.stringify(x)}`);
111
+ }
112
+ /**
113
+ * Evaluate a BoundExpr to produce a RuntimeValue.
114
+ */
115
+ export function evaluate(expr, ctx, session) {
116
+ switch (expr.kind) {
117
+ case BoundExprKind.Literal:
118
+ return evaluateLiteral(expr);
119
+ case BoundExprKind.CellRef:
120
+ return evaluateCellRef(expr, ctx, session);
121
+ case BoundExprKind.AreaRef:
122
+ return evaluateAreaRef(expr, ctx, session);
123
+ case BoundExprKind.ColRangeRef:
124
+ return evaluateColRange(expr, ctx, session);
125
+ case BoundExprKind.RowRangeRef:
126
+ return evaluateRowRange(expr, ctx, session);
127
+ case BoundExprKind.Ref3D:
128
+ return evaluateRef3D(expr, ctx, session);
129
+ case BoundExprKind.BinaryOp:
130
+ return evaluateBinaryOp(expr.op, expr.left, expr.right, ctx, session);
131
+ case BoundExprKind.UnaryOp:
132
+ return evaluateUnaryOp(expr.op, expr.operand, ctx, session);
133
+ case BoundExprKind.Percent:
134
+ return evaluatePercent(expr.operand, ctx, session);
135
+ case BoundExprKind.Call:
136
+ return evaluateCall(expr, ctx, session);
137
+ case BoundExprKind.SpecialCall:
138
+ return evaluateSpecialCall(expr, ctx, session);
139
+ case BoundExprKind.Array:
140
+ return evaluateArrayLiteral(expr, ctx, session);
141
+ case BoundExprKind.NameExpr:
142
+ return evaluateNameExpr(expr, ctx, session);
143
+ case BoundExprKind.Lambda:
144
+ return evaluateLambdaExpr(expr, ctx);
145
+ case BoundExprKind.StructuredRef:
146
+ return evaluateStructuredRef(expr, ctx, session);
147
+ default:
148
+ return assertNever(expr);
149
+ }
150
+ }
151
+ // ============================================================================
152
+ // Literal
153
+ // ============================================================================
154
+ function evaluateLiteral(expr) {
155
+ if (expr.errorCode) {
156
+ return rvError(expr.errorCode);
157
+ }
158
+ if (expr.value === null) {
159
+ return BLANK;
160
+ }
161
+ if (typeof expr.value === "number") {
162
+ return rvNumber(expr.value);
163
+ }
164
+ if (typeof expr.value === "string") {
165
+ return rvString(expr.value);
166
+ }
167
+ if (typeof expr.value === "boolean") {
168
+ return rvBoolean(expr.value);
169
+ }
170
+ return BLANK;
171
+ }
172
+ // ============================================================================
173
+ // Cell Reference
174
+ // ============================================================================
175
+ function evaluateCellRef(expr, ctx, session) {
176
+ return rvCellRef(expr.sheet, expr.row, expr.col);
177
+ }
178
+ // ============================================================================
179
+ // Area Reference → ReferenceValue
180
+ // ============================================================================
181
+ function evaluateAreaRef(expr, ctx, session) {
182
+ return rvRef(expr.sheet, expr.top, expr.left, expr.bottom, expr.right);
183
+ }
184
+ function evaluateColRange(expr, ctx, session) {
185
+ const ws = ctx.snapshot.worksheetsByName.get(expr.sheet.toLowerCase());
186
+ if (!ws || !ws.dimensions) {
187
+ return rvArray([]);
188
+ }
189
+ return rvRef(expr.sheet, ws.dimensions.top, expr.leftCol, ws.dimensions.bottom, expr.rightCol);
190
+ }
191
+ function evaluateRowRange(expr, ctx, session) {
192
+ const ws = ctx.snapshot.worksheetsByName.get(expr.sheet.toLowerCase());
193
+ if (!ws || !ws.dimensions) {
194
+ return rvArray([]);
195
+ }
196
+ return rvRef(expr.sheet, expr.topRow, ws.dimensions.left, expr.bottomRow, ws.dimensions.right);
197
+ }
198
+ function evaluateRef3D(expr, ctx, session) {
199
+ const areas = [];
200
+ for (const sheet of expr.sheets) {
201
+ if (expr.inner.kind === BoundExprKind.CellRef) {
202
+ areas.push({
203
+ sheet,
204
+ top: expr.inner.row,
205
+ left: expr.inner.col,
206
+ bottom: expr.inner.row,
207
+ right: expr.inner.col
208
+ });
209
+ }
210
+ else {
211
+ // For AreaRef: if the inner range is a whole-column or whole-row
212
+ // reference (top = 1 & bottom = 1048576, or left = 1 & right =
213
+ // 16384), clamp it against each sheet's actual used dimensions.
214
+ // Without this clamp, a 3D reference like `Sheet1:Sheet3!A:A`
215
+ // would allocate 3 × 1M rows of BLANK values and spend seconds
216
+ // (or OOM) before SUM even starts. The non-3D path already does
217
+ // this clamp in `evaluateColRange`; parity with that is what we
218
+ // want here.
219
+ let top = expr.inner.top;
220
+ let left = expr.inner.left;
221
+ let bottom = expr.inner.bottom;
222
+ let right = expr.inner.right;
223
+ const isWholeCol = top === 1 && bottom === 1048576;
224
+ const isWholeRow = left === 1 && right === 16384;
225
+ if (isWholeCol || isWholeRow) {
226
+ const ws = ctx.snapshot.worksheetsByName.get(sheet.toLowerCase());
227
+ const dims = ws?.dimensions;
228
+ if (dims) {
229
+ if (isWholeCol) {
230
+ top = dims.top;
231
+ bottom = dims.bottom;
232
+ }
233
+ if (isWholeRow) {
234
+ left = dims.left;
235
+ right = dims.right;
236
+ }
237
+ }
238
+ }
239
+ areas.push({ sheet, top, left, bottom, right });
240
+ }
241
+ }
242
+ return { kind: RVKind.Reference, areas };
243
+ }
244
+ // ============================================================================
245
+ // Build Range Array from Snapshot
246
+ // ============================================================================
247
+ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
248
+ // Hoist the worksheet lookup once for the entire range — avoids
249
+ // N redundant toLowerCase()/Map.get() calls inside the hot loop.
250
+ const ws = ctx.snapshot.worksheetsByName.get(sheet.toLowerCase());
251
+ const cells = ws?.cells;
252
+ const wsHiddenRows = ws?.hiddenRows;
253
+ const compiledFormulas = ctx.compiledFormulas;
254
+ const resultCache = session.resultCache;
255
+ // Hoist the recording guard — when not recording, skip recordAccess
256
+ // entirely in the loop instead of paying the function-call overhead.
257
+ const recording = session.recordingKey !== null;
258
+ const rows = [];
259
+ // Lazily-allocated masks: only materialized once we encounter a
260
+ // SUBTOTAL/AGGREGATE cell or a hidden row inside the range. For the
261
+ // common case (plain data range, no hidden rows) we never touch these
262
+ // and emit an ArrayValue without extra metadata.
263
+ let subtotalMask;
264
+ let hiddenRowMask;
265
+ const height = bottom - top + 1;
266
+ const width = right - left + 1;
267
+ for (let r = top; r <= bottom; r++) {
268
+ const row = [];
269
+ const ri = r - top;
270
+ // Record row visibility — SUBTOTAL 1xx / AGGREGATE opt 5/7 use it.
271
+ if (wsHiddenRows?.has(r)) {
272
+ if (!hiddenRowMask) {
273
+ hiddenRowMask = new Array(height).fill(false);
274
+ }
275
+ hiddenRowMask[ri] = true;
276
+ }
277
+ for (let c = left; c <= right; c++) {
278
+ if (recording) {
279
+ session.recordAccess(sheet, r, c);
280
+ }
281
+ // Missing worksheet: matches getCellValue's BLANK fallback.
282
+ if (!cells) {
283
+ row.push(BLANK);
284
+ continue;
285
+ }
286
+ const cell = cells.get(snapshotCellKey(r, c));
287
+ if (!cell) {
288
+ // No snapshot cell yet — might still be inside a live spill
289
+ // (e.g. reading A2..A5 while A1 = SEQUENCE(5) is still being
290
+ // materialized). See `readLiveSpill` for the lookup path.
291
+ const live = readLiveSpill(sheet, r, c, session);
292
+ row.push(live ? topLeft(live) : BLANK);
293
+ continue;
294
+ }
295
+ if (cell.formulaKind !== "none" && cell.formula) {
296
+ const fKey = formulaCellKey(sheet, r, c);
297
+ const compiled = compiledFormulas.get(fKey);
298
+ // Mark SUBTOTAL/AGGREGATE output cells so an outer SUBTOTAL /
299
+ // AGGREGATE over this range knows to skip them (Excel's
300
+ // no-double-count semantics).
301
+ if (compiled?.isSubtotalOutput) {
302
+ if (!subtotalMask) {
303
+ subtotalMask = new Array(height);
304
+ for (let i = 0; i < height; i++) {
305
+ subtotalMask[i] = new Array(width).fill(false);
306
+ }
307
+ }
308
+ subtotalMask[ri][c - left] = true;
309
+ }
310
+ const cached = resultCache.get(fKey);
311
+ if (cached !== undefined) {
312
+ row.push(topLeft(cached.scalar));
313
+ continue;
314
+ }
315
+ if (compiled) {
316
+ row.push(topLeft(evaluateFormula(compiled, ctx, session)));
317
+ continue;
318
+ }
319
+ }
320
+ // Non-formula snapshot cell — but it might still be a ghost slot
321
+ // that a fresh dynamic-array spill is about to overwrite. Prefer
322
+ // the live value when a master is registered so this-pass SUM /
323
+ // LOOKUP / etc. see the new spill immediately.
324
+ const live = readLiveSpill(sheet, r, c, session);
325
+ if (live) {
326
+ row.push(topLeft(live));
327
+ continue;
328
+ }
329
+ row.push(topLeft(fromSnapshotValue(cell.value)));
330
+ }
331
+ rows.push(row);
332
+ }
333
+ return rvArray(rows, top, left, subtotalMask, hiddenRowMask);
334
+ }
335
+ // ============================================================================
336
+ // Dereference: Reference → concrete value
337
+ // ============================================================================
338
+ /**
339
+ * Resolve a `ReferenceValue` to its concrete value (scalar or array).
340
+ * - Single-cell references (from CellRef nodes) resolve to a scalar.
341
+ * - Area references (even 1x1 from A1:A1) produce an array.
342
+ * - Multi-area references (from 3D refs) flatten all areas into one array.
343
+ * Non-reference values are returned unchanged.
344
+ */
345
+ function dereferenceValue(v, ctx, session) {
346
+ if (v.kind !== RVKind.Reference) {
347
+ return v;
348
+ }
349
+ if (v.areas.length === 0) {
350
+ return BLANK;
351
+ }
352
+ // Single-cell references (from CellRef nodes) resolve to scalar
353
+ if (v.singleCell) {
354
+ const area = v.areas[0];
355
+ return getCellValue(area.sheet, area.top, area.left, ctx, session);
356
+ }
357
+ // Single area — build range array
358
+ if (v.areas.length === 1) {
359
+ const area = v.areas[0];
360
+ return buildRangeArray(ctx, session, area.sheet, area.top, area.left, area.bottom, area.right);
361
+ }
362
+ // Multi-area (3D reference) — flatten all areas into one array
363
+ const allRows = [];
364
+ let mergedSubtotal;
365
+ let mergedHidden;
366
+ for (const area of v.areas) {
367
+ const arr = buildRangeArray(ctx, session, area.sheet, area.top, area.left, area.bottom, area.right);
368
+ const startRow = allRows.length;
369
+ for (const row of arr.rows) {
370
+ allRows.push([...row]);
371
+ }
372
+ // Merge masks from this area into the flattened output. Only
373
+ // allocate the merged masks when the first masked area shows up —
374
+ // most multi-area refs have no masks and should pay no overhead.
375
+ if (arr.subtotalMask || arr.hiddenRowMask) {
376
+ const height = arr.height;
377
+ if (arr.subtotalMask) {
378
+ if (!mergedSubtotal) {
379
+ mergedSubtotal = [];
380
+ for (let i = 0; i < startRow; i++) {
381
+ // Widths may differ across areas; mask rows use each area's
382
+ // own width so an outer SUBTOTAL reads the right positions.
383
+ mergedSubtotal.push([]);
384
+ }
385
+ }
386
+ for (let r = 0; r < height; r++) {
387
+ mergedSubtotal.push([...arr.subtotalMask[r]]);
388
+ }
389
+ }
390
+ else if (mergedSubtotal) {
391
+ // Pad with empty rows so indices stay aligned.
392
+ for (let r = 0; r < height; r++) {
393
+ mergedSubtotal.push([]);
394
+ }
395
+ }
396
+ if (arr.hiddenRowMask) {
397
+ if (!mergedHidden) {
398
+ mergedHidden = new Array(startRow).fill(false);
399
+ }
400
+ for (let r = 0; r < height; r++) {
401
+ mergedHidden.push(arr.hiddenRowMask[r] ?? false);
402
+ }
403
+ }
404
+ else if (mergedHidden) {
405
+ for (let r = 0; r < height; r++) {
406
+ mergedHidden.push(false);
407
+ }
408
+ }
409
+ }
410
+ else if (mergedSubtotal || mergedHidden) {
411
+ // An earlier area had masks; pad this area's rows with non-masked
412
+ // entries to keep row indices aligned.
413
+ const height = arr.height;
414
+ if (mergedSubtotal) {
415
+ for (let r = 0; r < height; r++) {
416
+ mergedSubtotal.push([]);
417
+ }
418
+ }
419
+ if (mergedHidden) {
420
+ for (let r = 0; r < height; r++) {
421
+ mergedHidden.push(false);
422
+ }
423
+ }
424
+ }
425
+ }
426
+ return rvArray(allRows, undefined, undefined, mergedSubtotal, mergedHidden);
427
+ }
428
+ // ============================================================================
429
+ // Get Cell Value from Snapshot
430
+ // ============================================================================
431
+ function getCellValue(sheetName, row, col, ctx, session) {
432
+ // Record this access for runtime dependency tracking. Inline guard
433
+ // avoids the function call overhead in the common case where no
434
+ // recording is active (formulas without dynamic refs).
435
+ if (session.recordingKey !== null) {
436
+ session.recordAccess(sheetName, row, col);
437
+ }
438
+ const ws = ctx.snapshot.worksheetsByName.get(sheetName.toLowerCase());
439
+ if (!ws) {
440
+ return BLANK;
441
+ }
442
+ const cellKey = snapshotCellKey(row, col);
443
+ const cell = ws.cells.get(cellKey);
444
+ if (!cell) {
445
+ // The cell isn't in the snapshot, but we might still have a live
446
+ // spill value for it — look up the master formula and extract the
447
+ // right array element. Matters when a downstream formula like
448
+ // `SUM(A1:A5)` runs before materialize writes the ghost cells for
449
+ // `A1 = SEQUENCE(5)` into the snapshot.
450
+ return readLiveSpill(sheetName, row, col, session) ?? BLANK;
451
+ }
452
+ // If this cell has a formula, evaluate it
453
+ if (cell.formulaKind !== "none" && cell.formula) {
454
+ const fKey = formulaCellKey(sheetName, row, col);
455
+ // Check cache — return scalar form for dependency resolution
456
+ const cached = session.resultCache.get(fKey);
457
+ if (cached !== undefined) {
458
+ return cached.scalar;
459
+ }
460
+ // Get compiled formula
461
+ const compiled = ctx.compiledFormulas.get(fKey);
462
+ if (compiled) {
463
+ return evaluateFormula(compiled, ctx, session);
464
+ }
465
+ }
466
+ // Non-formula cell — but it might also be a ghost for a live spill
467
+ // (e.g. a value that exists in the snapshot from a previous calc
468
+ // cycle but is about to be overwritten by a fresh spill). Prefer the
469
+ // live value when a master is registered.
470
+ const spill = readLiveSpill(sheetName, row, col, session);
471
+ if (spill) {
472
+ return spill;
473
+ }
474
+ return fromSnapshotValue(cell.value);
475
+ }
476
+ /**
477
+ * Retrieve the spill-target value at (sheetName, row, col) from an
478
+ * already-evaluated dynamic-array master. Returns `undefined` when no
479
+ * master is registered for that cell — callers fall back to the
480
+ * snapshot value or BLANK.
481
+ */
482
+ function readLiveSpill(sheetName, row, col, session) {
483
+ const key = session.makeKey(sheetName, row, col);
484
+ const spill = session.liveSpills.get(key);
485
+ if (!spill) {
486
+ return undefined;
487
+ }
488
+ const master = session.resultCache.get(spill.masterKey);
489
+ if (!master || master.raw.kind !== RVKind.Array) {
490
+ return undefined;
491
+ }
492
+ const arr = master.raw;
493
+ if (spill.rowOffset >= arr.height || spill.colOffset >= arr.width) {
494
+ return undefined;
495
+ }
496
+ return arr.rows[spill.rowOffset][spill.colOffset];
497
+ }
498
+ // ============================================================================
499
+ // Evaluate a Compiled Formula
500
+ // ============================================================================
501
+ /**
502
+ * Shared implementation for evaluateFormula and evaluateFormulaRaw.
503
+ *
504
+ * Handles key computation, cache lookup, circular reference detection,
505
+ * expression evaluation, and result caching.
506
+ */
507
+ function evaluateFormulaInner(compiled, ctx, session) {
508
+ const inst = compiled.instance;
509
+ const key = session.makeKey(inst.sheetName, inst.row, inst.col);
510
+ // Check cache
511
+ const cached = session.resultCache.get(key);
512
+ if (cached !== undefined) {
513
+ return cached;
514
+ }
515
+ // Circular reference detection. Under iterative calculation the driver
516
+ // (calculate-formulas-impl.ts) seeds `circularFallback` with the previous
517
+ // iteration's result so the re-entrant lookup receives a stable value.
518
+ // Outside of iterative mode the map is empty — we return 0 as the fallback,
519
+ // matching Excel's "iterate with 0 seed" convention. This keeps simple
520
+ // cycles like A1=A1+1 producing a number instead of an error, which is the
521
+ // established behaviour for this engine (tests depend on this). For strict
522
+ // circular-reference error reporting, enable iterative calculation and
523
+ // observe convergence failure, or configure a custom fallback value.
524
+ if (session.evaluating.has(key)) {
525
+ const fallback = session.circularFallback.get(key);
526
+ const val = fallback !== undefined ? fallback : rvNumber(0);
527
+ return { scalar: val, raw: val };
528
+ }
529
+ session.evaluating.add(key);
530
+ const prevAddress = ctx.currentAddress;
531
+ const prevSheet = ctx.currentSheet;
532
+ const prevRecording = session.recordingKey;
533
+ ctx.currentAddress = { sheet: inst.sheetName, row: inst.row, col: inst.col };
534
+ ctx.currentSheet = inst.sheetName;
535
+ // Enable runtime dependency recording for formulas with dynamic refs
536
+ if (compiled.hasDynamicRefs) {
537
+ session.recordingKey = key;
538
+ }
539
+ try {
540
+ const result = evaluate(compiled.bound, ctx, session);
541
+ const intersected = implicitIntersect(result, ctx);
542
+ const scalar = dereferenceValue(intersected, ctx, session);
543
+ const raw = dereferenceValue(result, ctx, session);
544
+ const entry = { scalar, raw };
545
+ session.resultCache.set(key, entry);
546
+ // Register the spill region if this is a dynamic-array formula
547
+ // whose result is a multi-cell array. Downstream formulas that
548
+ // read into the spill range (e.g. `=SUM(A1:A5)` over a
549
+ // `=SEQUENCE(5)` master) can now pick up the ghost-cell values
550
+ // before materialize writes them back to the snapshot.
551
+ const isDyn = compiled.instance.isDynamicArray || compiled.isDynamicArrayFunction;
552
+ if (isDyn && raw.kind === RVKind.Array && (raw.height > 1 || raw.width > 1)) {
553
+ for (let r = 0; r < raw.height; r++) {
554
+ for (let c = 0; c < raw.width; c++) {
555
+ if (r === 0 && c === 0) {
556
+ continue; // master cell already points to its own cache entry
557
+ }
558
+ const targetKey = session.makeKey(inst.sheetName, inst.row + r, inst.col + c);
559
+ session.liveSpills.set(targetKey, {
560
+ masterKey: key,
561
+ rowOffset: r,
562
+ colOffset: c
563
+ });
564
+ }
565
+ }
566
+ }
567
+ return entry;
568
+ }
569
+ catch (err) {
570
+ // Cache a #CALC! sentinel so a re-entrant lookup for the same cell does
571
+ // not trigger repeated (exponentially growing) re-evaluation under
572
+ // iterative calc or dependent recomputation. The exception is re-thrown
573
+ // so the outer caller can still log / translate it into a sheet error.
574
+ const fallback = { scalar: ERRORS.CALC, raw: ERRORS.CALC };
575
+ session.resultCache.set(key, fallback);
576
+ throw err;
577
+ }
578
+ finally {
579
+ session.evaluating.delete(key);
580
+ ctx.currentAddress = prevAddress;
581
+ ctx.currentSheet = prevSheet;
582
+ session.recordingKey = prevRecording;
583
+ }
584
+ }
585
+ /**
586
+ * Evaluate a compiled formula and return its **scalar** result.
587
+ *
588
+ * This is the standard evaluation path for regular (non-array) formulas.
589
+ * The result is:
590
+ * 1. Evaluated from the bound expression tree
591
+ * 2. Implicit-intersected to a single value/reference
592
+ * 3. Dereferenced if it's a reference
593
+ * 4. Cached for subsequent lookups by dependent formulas
594
+ *
595
+ * Use `evaluateFormulaRaw` instead when the full array result is needed
596
+ * (dynamic array formulas, CSE formulas).
597
+ */
598
+ export function evaluateFormula(compiled, ctx, session) {
599
+ return evaluateFormulaInner(compiled, ctx, session).scalar;
600
+ }
601
+ /**
602
+ * Evaluate a compiled formula and return the **raw** (possibly array) result.
603
+ *
604
+ * This is the evaluation path for dynamic array and CSE formulas where
605
+ * the full array shape must be preserved for spill/distribution.
606
+ *
607
+ * Semantics:
608
+ * - Both scalar and raw forms are stored in `session.resultCache` as a
609
+ * `CachedResult{scalar, raw}`. Dependent scalar formulas see the scalar
610
+ * form; the materialize layer retrieves the raw form.
611
+ * - The return value is the full dereferenced result — may be an ArrayValue
612
+ * with height > 1 or width > 1.
613
+ */
614
+ export function evaluateFormulaRaw(compiled, ctx, session) {
615
+ return evaluateFormulaInner(compiled, ctx, session).raw;
616
+ }
617
+ // ============================================================================
618
+ // Binary Operations
619
+ // ============================================================================
620
+ function evaluateBinaryOp(op, leftExpr, rightExpr, ctx, session) {
621
+ // Intersection operator (whitespace between two refs). Must be handled
622
+ // BEFORE dereferencing so we can inspect the reference areas.
623
+ if (op === " ") {
624
+ return evaluateIntersection(leftExpr, rightExpr, ctx, session);
625
+ }
626
+ // Range operator `:` — union of two references into the bounding
627
+ // rectangle. Needed when one side is a function call (e.g.
628
+ // `B11:INDIRECT("B" & ROW()-1)`). Both sides must be references or
629
+ // coerce to references; otherwise Excel returns #REF!.
630
+ if (op === ":") {
631
+ return evaluateRangeUnion(leftExpr, rightExpr, ctx, session);
632
+ }
633
+ const left = dereferenceValue(evaluate(leftExpr, ctx, session), ctx, session);
634
+ const right = dereferenceValue(evaluate(rightExpr, ctx, session), ctx, session);
635
+ const lIsArr = left.kind === RVKind.Array;
636
+ const rIsArr = right.kind === RVKind.Array;
637
+ if (lIsArr || rIsArr) {
638
+ return broadcastBinaryOp(op, left, right);
639
+ }
640
+ return applyScalarBinaryOp(op, topLeft(left), topLeft(right));
641
+ }
642
+ /**
643
+ * Excel's intersection operator — a whitespace character separating two
644
+ * references (e.g. `A1:A10 B1:B10`).
645
+ *
646
+ * Semantics:
647
+ * - Both operands must evaluate to single-area references. Otherwise the
648
+ * result is `#VALUE!` (matches Excel's behaviour when non-refs or
649
+ * multi-area refs are intersected).
650
+ * - Intersection is the rectangle overlap of the two areas on the same
651
+ * sheet.
652
+ * - If the areas do not overlap (or are on different sheets) the result
653
+ * is `#NULL!`, Excel's canonical "empty intersection" error.
654
+ */
655
+ function evaluateIntersection(leftExpr, rightExpr, ctx, session) {
656
+ const left = evaluate(leftExpr, ctx, session);
657
+ const right = evaluate(rightExpr, ctx, session);
658
+ if (isError(left)) {
659
+ return left;
660
+ }
661
+ if (isError(right)) {
662
+ return right;
663
+ }
664
+ if (left.kind !== RVKind.Reference || right.kind !== RVKind.Reference) {
665
+ return ERRORS.VALUE;
666
+ }
667
+ if (left.areas.length !== 1 || right.areas.length !== 1) {
668
+ return ERRORS.VALUE;
669
+ }
670
+ const la = left.areas[0];
671
+ const ra = right.areas[0];
672
+ if (la.sheet.toLowerCase() !== ra.sheet.toLowerCase()) {
673
+ return ERRORS.NULL;
674
+ }
675
+ const top = Math.max(la.top, ra.top);
676
+ const left_ = Math.max(la.left, ra.left);
677
+ const bottom = Math.min(la.bottom, ra.bottom);
678
+ const right_ = Math.min(la.right, ra.right);
679
+ if (top > bottom || left_ > right_) {
680
+ return ERRORS.NULL;
681
+ }
682
+ return rvRef(la.sheet, top, left_, bottom, right_);
683
+ }
684
+ /**
685
+ * Range operator `:` applied at runtime. Normally `A1:B2` is merged by
686
+ * the tokenizer, but patterns like `A1:INDIRECT("B5")` leave the colon
687
+ * as a standalone operator. Both operands must evaluate to references
688
+ * (single-cell or area); the result is the bounding rectangle of the
689
+ * two reference ranges on the same sheet.
690
+ *
691
+ * Semantics:
692
+ * - Both sides must be references. Literal numbers / strings → #VALUE!.
693
+ * - References must live on the same sheet → else #REF!.
694
+ * - Multi-area references on either side → #REF! (Excel behavior).
695
+ */
696
+ function evaluateRangeUnion(leftExpr, rightExpr, ctx, session) {
697
+ const left = evaluate(leftExpr, ctx, session);
698
+ const right = evaluate(rightExpr, ctx, session);
699
+ if (isError(left)) {
700
+ return left;
701
+ }
702
+ if (isError(right)) {
703
+ return right;
704
+ }
705
+ if (left.kind !== RVKind.Reference || right.kind !== RVKind.Reference) {
706
+ return ERRORS.VALUE;
707
+ }
708
+ if (left.areas.length !== 1 || right.areas.length !== 1) {
709
+ return ERRORS.REF;
710
+ }
711
+ const la = left.areas[0];
712
+ const ra = right.areas[0];
713
+ if (la.sheet.toLowerCase() !== ra.sheet.toLowerCase()) {
714
+ return ERRORS.REF;
715
+ }
716
+ // Bounding rectangle that spans both areas — this is the union /
717
+ // range-op semantics (Excel), distinct from intersection which uses
718
+ // min/max in the opposite direction.
719
+ const top = Math.min(la.top, ra.top);
720
+ const left_ = Math.min(la.left, ra.left);
721
+ const bottom = Math.max(la.bottom, ra.bottom);
722
+ const right_ = Math.max(la.right, ra.right);
723
+ return rvRef(la.sheet, top, left_, bottom, right_);
724
+ }
725
+ function applyScalarBinaryOp(op, left, right) {
726
+ if (isError(left)) {
727
+ return left;
728
+ }
729
+ if (isError(right)) {
730
+ return right;
731
+ }
732
+ // Concatenation
733
+ if (op === "&") {
734
+ const lStr = toStringRV(left);
735
+ const rStr = toStringRV(right);
736
+ return rvString(lStr + rStr);
737
+ }
738
+ // Comparison
739
+ if (op === "=" || op === "<>" || op === "<" || op === ">" || op === "<=" || op === ">=") {
740
+ return rvBoolean(compareScalars(left, right, op));
741
+ }
742
+ // Arithmetic
743
+ const lNum = toNumberRV(left);
744
+ if (isError(lNum)) {
745
+ return lNum;
746
+ }
747
+ const rNum = toNumberRV(right);
748
+ if (isError(rNum)) {
749
+ return rNum;
750
+ }
751
+ let result;
752
+ switch (op) {
753
+ case "+":
754
+ result = lNum.value + rNum.value;
755
+ break;
756
+ case "-":
757
+ result = lNum.value - rNum.value;
758
+ break;
759
+ case "*":
760
+ result = lNum.value * rNum.value;
761
+ break;
762
+ case "/":
763
+ if (rNum.value === 0) {
764
+ return ERRORS.DIV0;
765
+ }
766
+ result = lNum.value / rNum.value;
767
+ break;
768
+ case "^":
769
+ // Excel distinguishes `0 ^ n` for n < 0 (→ #DIV/0!, since it's
770
+ // semantically 1/0) from other overflows (→ #NUM!). The generic
771
+ // `isFinite` check below loses that distinction, so route the
772
+ // division-by-zero case explicitly first. 0^0 is conventionally 1
773
+ // (matches Excel and POWER()).
774
+ if (lNum.value === 0) {
775
+ if (rNum.value < 0) {
776
+ return ERRORS.DIV0;
777
+ }
778
+ if (rNum.value === 0) {
779
+ return rvNumber(1);
780
+ }
781
+ }
782
+ result = Math.pow(lNum.value, rNum.value);
783
+ if (Number.isNaN(result)) {
784
+ // `Math.pow(-1, 0.5)` etc. — complex result; Excel reports #NUM!.
785
+ return ERRORS.NUM;
786
+ }
787
+ break;
788
+ default:
789
+ return ERRORS.VALUE;
790
+ }
791
+ return !isFinite(result) ? ERRORS.NUM : rvNumber(result);
792
+ }
793
+ function compareScalars(left, right, op) {
794
+ // Normalize blanks to a neutral form of the opposing kind so formulas like
795
+ // `"" = A1` (where A1 is blank) compare equal. Without this normalisation
796
+ // Excel would route us to the cross-type tiebreak below.
797
+ const l = left.kind === RVKind.Blank
798
+ ? right.kind === RVKind.String
799
+ ? rvString("")
800
+ : right.kind === RVKind.Boolean
801
+ ? rvBoolean(false)
802
+ : rvNumber(0)
803
+ : left;
804
+ const r = right.kind === RVKind.Blank
805
+ ? left.kind === RVKind.String
806
+ ? rvString("")
807
+ : left.kind === RVKind.Boolean
808
+ ? rvBoolean(false)
809
+ : rvNumber(0)
810
+ : right;
811
+ let cmp;
812
+ if (l.kind === r.kind) {
813
+ cmp = compareScalarsSameKind(l, r);
814
+ if (!Number.isFinite(cmp)) {
815
+ cmp = 0;
816
+ }
817
+ }
818
+ else {
819
+ // Excel orders scalar kinds: Number < String < Boolean < Error/Blank.
820
+ const order = (v) => {
821
+ if (v.kind === RVKind.Number) {
822
+ return 0;
823
+ }
824
+ if (v.kind === RVKind.String) {
825
+ return 1;
826
+ }
827
+ if (v.kind === RVKind.Boolean) {
828
+ return 2;
829
+ }
830
+ return 3;
831
+ };
832
+ cmp = order(l) - order(r);
833
+ }
834
+ switch (op) {
835
+ case "=":
836
+ return cmp === 0;
837
+ case "<>":
838
+ return cmp !== 0;
839
+ case "<":
840
+ return cmp < 0;
841
+ case ">":
842
+ return cmp > 0;
843
+ case "<=":
844
+ return cmp <= 0;
845
+ case ">=":
846
+ return cmp >= 0;
847
+ default:
848
+ return false;
849
+ }
850
+ }
851
+ function broadcastBinaryOp(op, left, right) {
852
+ const lArr = left.kind === RVKind.Array ? left : null;
853
+ const rArr = right.kind === RVKind.Array ? right : null;
854
+ const lRows = lArr ? lArr.height : 1;
855
+ const lCols = lArr ? lArr.width : 1;
856
+ const rRows = rArr ? rArr.height : 1;
857
+ const rCols = rArr ? rArr.width : 1;
858
+ const outRows = Math.max(lRows, rRows);
859
+ const outCols = Math.max(lCols, rCols);
860
+ if ((lRows !== 1 && rRows !== 1 && lRows !== rRows) ||
861
+ (lCols !== 1 && rCols !== 1 && lCols !== rCols)) {
862
+ return ERRORS.VALUE;
863
+ }
864
+ // Guard against pathological broadcasts (e.g. A:A * 1:1 = ~17B cells).
865
+ // 10M cells is well beyond any legitimate array use case.
866
+ if (outRows * outCols > 10000000) {
867
+ return ERRORS.CALC;
868
+ }
869
+ // Precompute scalar-broadcast values once outside the hot cell loop.
870
+ // When one side is a non-array `RuntimeValue`, it expands to the same
871
+ // `ScalarValue` for every (r, c); repeating `topLeft(left)` inside the
872
+ // inner loop (outRows × outCols calls) was pure overhead.
873
+ const lScalarFallback = lArr ? undefined : topLeft(left);
874
+ const rScalarFallback = rArr ? undefined : topLeft(right);
875
+ const rows = [];
876
+ for (let r = 0; r < outRows; r++) {
877
+ const row = [];
878
+ for (let c = 0; c < outCols; c++) {
879
+ const lR = lRows === 1 ? 0 : r;
880
+ const lC = lCols === 1 ? 0 : c;
881
+ const rR = rRows === 1 ? 0 : r;
882
+ const rC = rCols === 1 ? 0 : c;
883
+ // Array values are rectangular (normalised by rvArray) so direct
884
+ // indexing is safe; the previous `?? BLANK` fallback was defensive
885
+ // code that never triggered in practice but cost an optional chain
886
+ // per cell in a hot loop.
887
+ const lVal = lArr ? lArr.rows[lR][lC] : lScalarFallback;
888
+ const rVal = rArr ? rArr.rows[rR][rC] : rScalarFallback;
889
+ row.push(applyScalarBinaryOp(op, lVal, rVal));
890
+ }
891
+ rows.push(row);
892
+ }
893
+ // Propagate origin metadata
894
+ const originRow = lArr?.originRow ?? rArr?.originRow;
895
+ const originCol = lArr?.originCol ?? rArr?.originCol;
896
+ return rvArray(rows, originRow, originCol);
897
+ }
898
+ // ============================================================================
899
+ // Unary Operations
900
+ // ============================================================================
901
+ function evaluateUnaryOp(op, operandExpr, ctx, session) {
902
+ const rawVal = evaluate(operandExpr, ctx, session);
903
+ // @ implicit intersection
904
+ if (op === "@") {
905
+ const intersected = implicitIntersect(rawVal, ctx);
906
+ return dereferenceValue(intersected, ctx, session);
907
+ }
908
+ const val = dereferenceValue(rawVal, ctx, session);
909
+ if (val.kind === RVKind.Array) {
910
+ const rows = [];
911
+ for (const row of val.rows) {
912
+ rows.push(row.map(cell => applyScalarUnary(op, cell)));
913
+ }
914
+ return rvArray(rows, val.originRow, val.originCol);
915
+ }
916
+ return applyScalarUnary(op, topLeft(val));
917
+ }
918
+ function applyScalarUnary(op, val) {
919
+ if (isError(val)) {
920
+ return val;
921
+ }
922
+ const n = toNumberRV(val);
923
+ if (isError(n)) {
924
+ return n;
925
+ }
926
+ switch (op) {
927
+ case "-":
928
+ return rvNumber(-n.value);
929
+ case "+":
930
+ return n;
931
+ default:
932
+ return ERRORS.VALUE;
933
+ }
934
+ }
935
+ // ============================================================================
936
+ // Percent
937
+ // ============================================================================
938
+ function evaluatePercent(operandExpr, ctx, session) {
939
+ const val = dereferenceValue(evaluate(operandExpr, ctx, session), ctx, session);
940
+ if (val.kind === RVKind.Array) {
941
+ const rows = [];
942
+ for (const row of val.rows) {
943
+ rows.push(row.map(cell => {
944
+ if (isError(cell)) {
945
+ return cell;
946
+ }
947
+ const n = toNumberRV(cell);
948
+ if (isError(n)) {
949
+ return n;
950
+ }
951
+ return rvNumber(n.value / 100);
952
+ }));
953
+ }
954
+ return rvArray(rows);
955
+ }
956
+ const scalar = topLeft(val);
957
+ if (isError(scalar)) {
958
+ return scalar;
959
+ }
960
+ const n = toNumberRV(scalar);
961
+ if (isError(n)) {
962
+ return n;
963
+ }
964
+ return rvNumber(n.value / 100);
965
+ }
966
+ // ============================================================================
967
+ // Function Call (Eager)
968
+ // ============================================================================
969
+ function evaluateCall(expr, ctx, session) {
970
+ // Reference functions: ROW, COLUMN, ROWS, COLUMNS
971
+ // (Accept _XLFN. prefixed names transparently.)
972
+ const canonical = stripFunctionPrefix(expr.name);
973
+ const refResult = tryEvaluateRefFunction(canonical, expr.args, ctx);
974
+ if (refResult !== undefined) {
975
+ return refResult;
976
+ }
977
+ // Reference-producing functions like INDIRECT/OFFSET yield a
978
+ // ReferenceValue at runtime. ROW/COLUMN/ROWS/COLUMNS need to inspect
979
+ // the resulting reference's address rather than its dereferenced value,
980
+ // so we evaluate the argument *without* dereferencing and extract the
981
+ // geometry directly. Only the 1-arg reference-only forms go down this
982
+ // path; scalar/array arguments still fall through to the eager fallback
983
+ // below, which returns #VALUE! for ROW/COLUMN and the correct count for
984
+ // ROWS/COLUMNS.
985
+ if (expr.args.length === 1 && isSimpleRefFunction(canonical)) {
986
+ const raw = evaluate(expr.args[0], ctx, session);
987
+ if (raw.kind === RVKind.Reference && raw.areas.length > 0) {
988
+ const area = raw.areas[0];
989
+ switch (canonical) {
990
+ case "ROW":
991
+ return rvNumber(area.top);
992
+ case "COLUMN":
993
+ return rvNumber(area.left);
994
+ case "ROWS":
995
+ return rvNumber(area.bottom - area.top + 1);
996
+ case "COLUMNS":
997
+ return rvNumber(area.right - area.left + 1);
998
+ }
999
+ }
1000
+ }
1001
+ // ── Reference-aware functions (ISREF, CELL) ──
1002
+ // These inspect the argument's reference-ness rather than its dereferenced
1003
+ // value. Handled here so the raw BoundExpr / ReferenceValue is visible.
1004
+ if (canonical === "ISREF") {
1005
+ return evaluateISREF(expr.args, ctx, session);
1006
+ }
1007
+ if (canonical === "CELL") {
1008
+ return evaluateCELL(expr.args, ctx, session);
1009
+ }
1010
+ // Evaluate all arguments eagerly and dereference references
1011
+ const args = expr.args.map(arg => dereferenceValue(evaluate(arg, ctx, session), ctx, session));
1012
+ // Look up function
1013
+ const desc = lookupFunction(expr.name);
1014
+ if (desc) {
1015
+ // Validate arity — produce #VALUE! for wrong argument count
1016
+ if (args.length < desc.minArity || args.length > desc.maxArity) {
1017
+ return ERRORS.VALUE;
1018
+ }
1019
+ // Context-aware overrides for functions that need evaluator state
1020
+ switch (canonical) {
1021
+ case "SHEET": {
1022
+ // SHEET() → current sheet number; SHEET(ref) → sheet number of ref
1023
+ if (args.length === 0) {
1024
+ const idx = ctx.snapshot.worksheets.findIndex(ws => ws.name.toLowerCase() === ctx.currentSheet.toLowerCase());
1025
+ return rvNumber(idx >= 0 ? idx + 1 : 1);
1026
+ }
1027
+ return desc.invoke(args);
1028
+ }
1029
+ case "SHEETS": {
1030
+ // SHEETS() → total sheet count
1031
+ if (args.length === 0) {
1032
+ return rvNumber(ctx.snapshot.worksheets.length);
1033
+ }
1034
+ return desc.invoke(args);
1035
+ }
1036
+ case "ISFORMULA": {
1037
+ // ISFORMULA requires a reference argument. When the raw argument is
1038
+ // a CellRef/AreaRef we can look up the underlying cell's formulaKind.
1039
+ // Any other shape (literal, computed value, etc.) yields #N/A per
1040
+ // Excel's behavior for non-reference arguments.
1041
+ const raw = expr.args[0];
1042
+ if (raw && raw.kind === BoundExprKind.CellRef) {
1043
+ const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
1044
+ if (!ws) {
1045
+ return ERRORS.REF;
1046
+ }
1047
+ const cell = ws.cells.get(snapshotCellKey(raw.row, raw.col));
1048
+ return rvBoolean(cell !== undefined && cell.formulaKind !== "none");
1049
+ }
1050
+ if (raw && raw.kind === BoundExprKind.AreaRef) {
1051
+ const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
1052
+ if (!ws) {
1053
+ return ERRORS.REF;
1054
+ }
1055
+ // ISFORMULA on an area ref inspects the top-left cell.
1056
+ const cell = ws.cells.get(snapshotCellKey(raw.top, raw.left));
1057
+ return rvBoolean(cell !== undefined && cell.formulaKind !== "none");
1058
+ }
1059
+ return ERRORS.NA;
1060
+ }
1061
+ case "FORMULATEXT": {
1062
+ // FORMULATEXT returns the formula source text at the referenced cell,
1063
+ // or #N/A if the cell has no formula. Non-reference arguments also
1064
+ // yield #N/A.
1065
+ const raw = expr.args[0];
1066
+ if (raw && raw.kind === BoundExprKind.CellRef) {
1067
+ const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
1068
+ if (!ws) {
1069
+ return ERRORS.REF;
1070
+ }
1071
+ const cell = ws.cells.get(snapshotCellKey(raw.row, raw.col));
1072
+ if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
1073
+ return rvString(`=${cell.formula}`);
1074
+ }
1075
+ return ERRORS.NA;
1076
+ }
1077
+ if (raw && raw.kind === BoundExprKind.AreaRef) {
1078
+ const ws = ctx.snapshot.worksheetsByName.get(raw.sheet.toLowerCase());
1079
+ if (!ws) {
1080
+ return ERRORS.REF;
1081
+ }
1082
+ const cell = ws.cells.get(snapshotCellKey(raw.top, raw.left));
1083
+ if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
1084
+ return rvString(`=${cell.formula}`);
1085
+ }
1086
+ return ERRORS.NA;
1087
+ }
1088
+ return ERRORS.NA;
1089
+ }
1090
+ default:
1091
+ return desc.invoke(args);
1092
+ }
1093
+ }
1094
+ // Check if name resolves to a lambda (defined name or local binding)
1095
+ const lambda = resolveLambdaName(expr.name, args, ctx, session);
1096
+ if (lambda !== undefined) {
1097
+ return lambda;
1098
+ }
1099
+ return ERRORS.NAME;
1100
+ }
1101
+ // ============================================================================
1102
+ // Special Form Call (Lazy)
1103
+ // ============================================================================
1104
+ function evaluateSpecialCall(expr, ctx, session) {
1105
+ switch (expr.name) {
1106
+ case "IF":
1107
+ return evaluateIF(expr.args, ctx, session);
1108
+ case "IFERROR":
1109
+ return evaluateIFERROR(expr.args, ctx, session);
1110
+ case "IFNA":
1111
+ return evaluateIFNA(expr.args, ctx, session);
1112
+ case "IFS":
1113
+ return evaluateIFS(expr.args, ctx, session);
1114
+ case "SWITCH":
1115
+ return evaluateSWITCH(expr.args, ctx, session);
1116
+ case "CHOOSE":
1117
+ return evaluateCHOOSE(expr.args, ctx, session);
1118
+ case "LET":
1119
+ return evaluateLET(expr.args, ctx, session);
1120
+ case "LAMBDA":
1121
+ return evaluateLAMBDA(expr.args, ctx);
1122
+ case "INDIRECT":
1123
+ return evaluateINDIRECT(expr.args, ctx, session);
1124
+ case "OFFSET":
1125
+ return evaluateOFFSET(expr.args, ctx, session);
1126
+ case "MAP":
1127
+ case "REDUCE":
1128
+ case "SCAN":
1129
+ case "MAKEARRAY":
1130
+ case "BYROW":
1131
+ case "BYCOL":
1132
+ return evaluateHigherOrder(expr.name, expr.args, ctx, session);
1133
+ default:
1134
+ return ERRORS.VALUE;
1135
+ }
1136
+ }
1137
+ // ============================================================================
1138
+ // Special Forms Implementation
1139
+ // ============================================================================
1140
+ function evaluateIF(args, ctx, session) {
1141
+ if (args.length < 2) {
1142
+ return ERRORS.VALUE;
1143
+ }
1144
+ const condRaw = evalDeref(args[0], ctx, session);
1145
+ // Array condition: element-wise IF. Excel's dynamic-array mode makes
1146
+ // `IF({TRUE,FALSE,TRUE}, "Y", "N")` return `{"Y","N","Y"}`. The branches
1147
+ // are evaluated eagerly — Excel does this too because array broadcasting
1148
+ // requires both shapes to be known — and each cell in the output picks
1149
+ // from the corresponding cell of the chosen branch (with scalar branches
1150
+ // broadcasting to fill the condition array's shape).
1151
+ if (condRaw.kind === RVKind.Array) {
1152
+ const trueVal = evalDeref(args[1], ctx, session);
1153
+ const falseVal = args.length > 2 ? evalDeref(args[2], ctx, session) : rvBoolean(false);
1154
+ const rows = [];
1155
+ for (let r = 0; r < condRaw.height; r++) {
1156
+ const outRow = [];
1157
+ for (let c = 0; c < condRaw.width; c++) {
1158
+ const cell = condRaw.rows[r][c];
1159
+ if (cell.kind === RVKind.Error) {
1160
+ outRow.push(cell);
1161
+ continue;
1162
+ }
1163
+ const b = toBooleanRV(cell);
1164
+ if (b.kind === RVKind.Error) {
1165
+ outRow.push(b);
1166
+ continue;
1167
+ }
1168
+ const branch = b.value ? trueVal : falseVal;
1169
+ outRow.push(pickCellBroadcast(branch, r, c));
1170
+ }
1171
+ rows.push(outRow);
1172
+ }
1173
+ return rvArray(rows);
1174
+ }
1175
+ const cond = topLeft(condRaw);
1176
+ if (isError(cond)) {
1177
+ return cond;
1178
+ }
1179
+ const bool = toBooleanRV(cond);
1180
+ if (isError(bool)) {
1181
+ return bool;
1182
+ }
1183
+ if (bool.value) {
1184
+ return evaluate(args[1], ctx, session);
1185
+ }
1186
+ return args.length > 2 ? evaluate(args[2], ctx, session) : rvBoolean(false);
1187
+ }
1188
+ /**
1189
+ * Pick a scalar from `branch` corresponding to grid position (r, c), with
1190
+ * broadcasting: scalar branches are repeated; smaller arrays are indexed
1191
+ * modulo their bounds (out-of-range → BLANK) matching Excel's array
1192
+ * alignment rules for IF/IFS/etc.
1193
+ */
1194
+ function pickCellBroadcast(branch, r, c) {
1195
+ if (branch.kind !== RVKind.Array) {
1196
+ return topLeft(branch);
1197
+ }
1198
+ const row = r < branch.height ? r : branch.height === 1 ? 0 : -1;
1199
+ const col = c < branch.width ? c : branch.width === 1 ? 0 : -1;
1200
+ if (row < 0 || col < 0) {
1201
+ // Misaligned array branch (smaller than the condition, and not a
1202
+ // broadcastable 1-row / 1-column shape). Excel fills the gaps with
1203
+ // `#N/A` rather than BLANK, so downstream consumers can distinguish
1204
+ // "branch didn't cover this cell" from "branch actually returned
1205
+ // empty". (R6-P1-7)
1206
+ return ERRORS.NA;
1207
+ }
1208
+ return branch.rows[row][col];
1209
+ }
1210
+ /**
1211
+ * Shared array-aware error replacement used by IFERROR and IFNA. Scans `val`
1212
+ * for cells matching `isMatch`; if any are found, replaces each with the
1213
+ * top-left scalar of `replacement` (lazily evaluated). Scalar inputs follow
1214
+ * the same match-or-pass-through logic.
1215
+ */
1216
+ function replaceErrorsIn(val, args, ctx, session, isMatch) {
1217
+ if (val.kind === RVKind.Array) {
1218
+ let anyMatch = false;
1219
+ for (const row of val.rows) {
1220
+ for (const cell of row) {
1221
+ if (cell.kind === RVKind.Error && isMatch(cell)) {
1222
+ anyMatch = true;
1223
+ break;
1224
+ }
1225
+ }
1226
+ if (anyMatch) {
1227
+ break;
1228
+ }
1229
+ }
1230
+ if (!anyMatch) {
1231
+ return val;
1232
+ }
1233
+ const replaceScalar = topLeft(evalDeref(args[1], ctx, session));
1234
+ const rows = [];
1235
+ for (const row of val.rows) {
1236
+ const newRow = [];
1237
+ for (const cell of row) {
1238
+ newRow.push(cell.kind === RVKind.Error && isMatch(cell) ? replaceScalar : cell);
1239
+ }
1240
+ rows.push(newRow);
1241
+ }
1242
+ return rvArray(rows);
1243
+ }
1244
+ return isError(val) && isMatch(val) ? evalDeref(args[1], ctx, session) : val;
1245
+ }
1246
+ function evaluateIFERROR(args, ctx, session) {
1247
+ if (args.length < 2) {
1248
+ return ERRORS.VALUE;
1249
+ }
1250
+ const val = evalDeref(args[0], ctx, session);
1251
+ return replaceErrorsIn(val, args, ctx, session, () => true);
1252
+ }
1253
+ function evaluateIFNA(args, ctx, session) {
1254
+ if (args.length < 2) {
1255
+ return ERRORS.VALUE;
1256
+ }
1257
+ const val = evalDeref(args[0], ctx, session);
1258
+ return replaceErrorsIn(val, args, ctx, session, err => err.code === "#N/A");
1259
+ }
1260
+ function evaluateIFS(args, ctx, session) {
1261
+ if (args.length < 2) {
1262
+ return ERRORS.VALUE;
1263
+ }
1264
+ // Excel requires IFS args to come in test/value pairs. Odd-length
1265
+ // arg lists imply a trailing test with no value and are #N/A.
1266
+ if (args.length % 2 !== 0) {
1267
+ return ERRORS.NA;
1268
+ }
1269
+ for (let i = 0; i < args.length - 1; i += 2) {
1270
+ const cond = topLeft(evalDeref(args[i], ctx, session));
1271
+ if (isError(cond)) {
1272
+ return cond;
1273
+ }
1274
+ const bool = toBooleanRV(cond);
1275
+ if (isError(bool)) {
1276
+ return bool;
1277
+ }
1278
+ if (bool.value) {
1279
+ return evaluate(args[i + 1], ctx, session);
1280
+ }
1281
+ }
1282
+ return ERRORS.NA;
1283
+ }
1284
+ function evaluateSWITCH(args, ctx, session) {
1285
+ if (args.length < 3) {
1286
+ return ERRORS.VALUE;
1287
+ }
1288
+ const expr = topLeft(evalDeref(args[0], ctx, session));
1289
+ if (isError(expr)) {
1290
+ return expr;
1291
+ }
1292
+ for (let i = 1; i < args.length - 1; i += 2) {
1293
+ const caseVal = topLeft(evalDeref(args[i], ctx, session));
1294
+ if (scalarEquals(expr, caseVal)) {
1295
+ return evaluate(args[i + 1], ctx, session);
1296
+ }
1297
+ }
1298
+ if (args.length % 2 === 0) {
1299
+ return evaluate(args[args.length - 1], ctx, session);
1300
+ }
1301
+ return ERRORS.NA;
1302
+ }
1303
+ function evaluateCHOOSE(args, ctx, session) {
1304
+ if (args.length < 2) {
1305
+ return ERRORS.VALUE;
1306
+ }
1307
+ const idxVal = topLeft(evalDeref(args[0], ctx, session));
1308
+ if (isError(idxVal)) {
1309
+ return idxVal;
1310
+ }
1311
+ const num = toNumberRV(idxVal);
1312
+ if (isError(num)) {
1313
+ return num;
1314
+ }
1315
+ const idx = Math.floor(num.value);
1316
+ if (idx < 1 || idx >= args.length) {
1317
+ return ERRORS.VALUE;
1318
+ }
1319
+ return evaluate(args[idx], ctx, session);
1320
+ }
1321
+ function evaluateLET(args, ctx, session) {
1322
+ if (args.length < 3 || args.length % 2 !== 1) {
1323
+ return ERRORS.VALUE;
1324
+ }
1325
+ const prevBindings = ctx.localBindings;
1326
+ const newBindings = new Map(prevBindings);
1327
+ try {
1328
+ const pairCount = (args.length - 1) / 2;
1329
+ for (let i = 0; i < pairCount; i++) {
1330
+ const nameExpr = args[i * 2];
1331
+ const valueExpr = args[i * 2 + 1];
1332
+ if (nameExpr.kind !== BoundExprKind.NameExpr) {
1333
+ return ERRORS.VALUE;
1334
+ }
1335
+ ctx.localBindings = newBindings;
1336
+ const val = evaluate(valueExpr, ctx, session);
1337
+ newBindings.set(nameExpr.upperName, val);
1338
+ }
1339
+ ctx.localBindings = newBindings;
1340
+ return evaluate(args[args.length - 1], ctx, session);
1341
+ }
1342
+ finally {
1343
+ ctx.localBindings = prevBindings;
1344
+ }
1345
+ }
1346
+ function evaluateLAMBDA(args, ctx) {
1347
+ if (args.length < 1) {
1348
+ return ERRORS.VALUE;
1349
+ }
1350
+ const paramExprs = args.slice(0, -1);
1351
+ const bodyExpr = args[args.length - 1];
1352
+ const params = [];
1353
+ for (const p of paramExprs) {
1354
+ if (p.kind !== BoundExprKind.NameExpr) {
1355
+ return ERRORS.VALUE;
1356
+ }
1357
+ params.push(p.upperName);
1358
+ }
1359
+ return rvLambda(params, bodyExpr, ctx.localBindings ? new Map(ctx.localBindings) : undefined);
1360
+ }
1361
+ function evaluateINDIRECT(args, ctx, session) {
1362
+ if (args.length < 1) {
1363
+ return ERRORS.VALUE;
1364
+ }
1365
+ const refArg = evalDeref(args[0], ctx, session);
1366
+ const refText = toStringRV(topLeft(refArg));
1367
+ if (!refText) {
1368
+ return ERRORS.REF;
1369
+ }
1370
+ let a1 = true;
1371
+ if (args.length >= 2) {
1372
+ const a1Val = topLeft(evalDeref(args[1], ctx, session));
1373
+ a1 =
1374
+ !(a1Val.kind === RVKind.Boolean && !a1Val.value) &&
1375
+ !(a1Val.kind === RVKind.Number && a1Val.value === 0);
1376
+ }
1377
+ if (!a1) {
1378
+ // R1C1 — delegate to runtime R1C1 parser
1379
+ return resolveR1C1(refText, ctx, session);
1380
+ }
1381
+ // A1 style — parse and bind at runtime
1382
+ try {
1383
+ // Use NUL (U+0000) as the separator so sheet names containing `__`
1384
+ // can't collide with distinct INDIRECT call sites. Neither formula
1385
+ // text nor an Excel sheet name is allowed to contain `\0`, so the
1386
+ // key is unambiguous. (R6-P1-12)
1387
+ const cacheKey = `${ctx.currentSheet}\u0000${refText}`;
1388
+ let bound = session.indirectAstCache.get(cacheKey);
1389
+ if (bound) {
1390
+ // LRU touch: delete-then-set moves the hit entry to the Map's
1391
+ // insertion-order tail so the oldest entry is always `keys().next()`.
1392
+ session.indirectAstCache.delete(cacheKey);
1393
+ session.indirectAstCache.set(cacheKey, bound);
1394
+ }
1395
+ else {
1396
+ const tokens = tokenize(refText);
1397
+ const ast = parse(tokens);
1398
+ const bindCtx = { snapshot: ctx.snapshot, currentSheet: ctx.currentSheet };
1399
+ bound = bind(ast, bindCtx);
1400
+ // Bound the cache so an adversarial formula that generates a fresh
1401
+ // INDIRECT string every call can't grow session memory unbounded.
1402
+ // The cap matches `astCache` in calculate-formulas-impl.ts. (R6
1403
+ // architectural note #6)
1404
+ if (session.indirectAstCache.size >= 10000) {
1405
+ const oldestKey = session.indirectAstCache.keys().next().value;
1406
+ if (oldestKey !== undefined) {
1407
+ session.indirectAstCache.delete(oldestKey);
1408
+ }
1409
+ }
1410
+ session.indirectAstCache.set(cacheKey, bound);
1411
+ }
1412
+ return evaluate(bound, ctx, session);
1413
+ }
1414
+ catch {
1415
+ return ERRORS.REF;
1416
+ }
1417
+ }
1418
+ function evaluateOFFSET(args, ctx, session) {
1419
+ if (args.length < 3) {
1420
+ return ERRORS.VALUE;
1421
+ }
1422
+ const refExpr = args[0];
1423
+ let baseRow;
1424
+ let baseCol;
1425
+ let baseSheet;
1426
+ // Remember the base reference's shape — Excel's OFFSET uses it as the
1427
+ // default height/width when those optional arguments are omitted.
1428
+ let baseHeight;
1429
+ let baseWidth;
1430
+ if (refExpr.kind === BoundExprKind.CellRef) {
1431
+ baseRow = refExpr.row;
1432
+ baseCol = refExpr.col;
1433
+ baseSheet = refExpr.sheet;
1434
+ baseHeight = 1;
1435
+ baseWidth = 1;
1436
+ }
1437
+ else if (refExpr.kind === BoundExprKind.AreaRef) {
1438
+ baseRow = refExpr.top;
1439
+ baseCol = refExpr.left;
1440
+ baseSheet = refExpr.sheet;
1441
+ baseHeight = refExpr.bottom - refExpr.top + 1;
1442
+ baseWidth = refExpr.right - refExpr.left + 1;
1443
+ }
1444
+ else {
1445
+ return ERRORS.VALUE;
1446
+ }
1447
+ const rowsVal = topLeft(evalDeref(args[1], ctx, session));
1448
+ const rowsNum = toNumberRV(rowsVal);
1449
+ if (isError(rowsNum)) {
1450
+ return rowsNum;
1451
+ }
1452
+ const colsVal = topLeft(evalDeref(args[2], ctx, session));
1453
+ const colsNum = toNumberRV(colsVal);
1454
+ if (isError(colsNum)) {
1455
+ return colsNum;
1456
+ }
1457
+ // Excel truncates fractional rows/cols toward zero (not floor). Without
1458
+ // the `Math.trunc`, `OFFSET(A5, -0.7, 0)` would resolve to row 4.3 and
1459
+ // then fail the Map lookup silently, returning BLANK instead of A5.
1460
+ const newRow = baseRow + Math.trunc(rowsNum.value);
1461
+ const newCol = baseCol + Math.trunc(colsNum.value);
1462
+ if (newRow < 1 || newCol < 1 || newRow > 1048576 || newCol > 16384) {
1463
+ return ERRORS.REF;
1464
+ }
1465
+ // Default height / width come from the base reference itself. OFFSET
1466
+ // only shrinks/expands when the caller passes an explicit non-missing
1467
+ // fourth/fifth argument. A `MissingNode` (compile-time "omitted
1468
+ // argument") binds to a `null`-valued literal; we treat that the same
1469
+ // as "no argument provided" so `OFFSET(A1:C3, 0, 0, , )` keeps the
1470
+ // 3-row × 3-col span instead of collapsing to #REF! with `height = 0`.
1471
+ const isOmitted = (a) => a.kind === BoundExprKind.Literal && a.value === null && a.errorCode === undefined;
1472
+ let height = baseHeight;
1473
+ let width = baseWidth;
1474
+ if (args.length > 3 && !isOmitted(args[3])) {
1475
+ const h = toNumberRV(topLeft(evalDeref(args[3], ctx, session)));
1476
+ if (isError(h)) {
1477
+ return h;
1478
+ }
1479
+ height = Math.trunc(h.value);
1480
+ if (height === 0) {
1481
+ return ERRORS.REF;
1482
+ }
1483
+ }
1484
+ if (args.length > 4 && !isOmitted(args[4])) {
1485
+ const w = toNumberRV(topLeft(evalDeref(args[4], ctx, session)));
1486
+ if (isError(w)) {
1487
+ return w;
1488
+ }
1489
+ width = Math.trunc(w.value);
1490
+ if (width === 0) {
1491
+ return ERRORS.REF;
1492
+ }
1493
+ }
1494
+ // Resolve range coordinates — negative height/width extend upward/leftward
1495
+ let top = newRow;
1496
+ let bottom = newRow + height - 1;
1497
+ if (height < 0) {
1498
+ top = newRow + height + 1;
1499
+ bottom = newRow;
1500
+ }
1501
+ let left = newCol;
1502
+ let right = newCol + width - 1;
1503
+ if (width < 0) {
1504
+ left = newCol + width + 1;
1505
+ right = newCol;
1506
+ }
1507
+ if (top < 1 || left < 1 || bottom > 1048576 || right > 16384) {
1508
+ return ERRORS.REF;
1509
+ }
1510
+ if (top === bottom && left === right) {
1511
+ return getCellValue(baseSheet, top, left, ctx, session);
1512
+ }
1513
+ return buildRangeArray(ctx, session, baseSheet, top, left, bottom, right);
1514
+ }
1515
+ // ============================================================================
1516
+ // Higher-Order Functions
1517
+ // ============================================================================
1518
+ function evaluateHigherOrder(name, args, ctx, session) {
1519
+ switch (name) {
1520
+ case "MAP":
1521
+ return evaluateMAP(args, ctx, session);
1522
+ case "REDUCE":
1523
+ return evaluateREDUCE(args, ctx, session);
1524
+ case "SCAN":
1525
+ return evaluateSCAN(args, ctx, session);
1526
+ case "MAKEARRAY":
1527
+ return evaluateMAKEARRAY(args, ctx, session);
1528
+ case "BYROW":
1529
+ return evaluateBYROW(args, ctx, session);
1530
+ case "BYCOL":
1531
+ return evaluateBYCOL(args, ctx, session);
1532
+ default:
1533
+ return ERRORS.VALUE;
1534
+ }
1535
+ }
1536
+ function invokeLambda(lambda, lambdaArgs, ctx, session) {
1537
+ if (lambdaArgs.length !== lambda.params.length) {
1538
+ return ERRORS.VALUE;
1539
+ }
1540
+ if (session.lambdaDepth >= 256) {
1541
+ return ERRORS.NUM;
1542
+ }
1543
+ session.lambdaDepth++;
1544
+ try {
1545
+ const prevBindings = ctx.localBindings;
1546
+ const newBindings = new Map(lambda.closureBindings);
1547
+ for (let i = 0; i < lambda.params.length; i++) {
1548
+ newBindings.set(lambda.params[i], lambdaArgs[i]);
1549
+ }
1550
+ ctx.localBindings = newBindings;
1551
+ try {
1552
+ return dereferenceValue(evaluate(lambda.body, ctx, session), ctx, session);
1553
+ }
1554
+ finally {
1555
+ ctx.localBindings = prevBindings;
1556
+ }
1557
+ }
1558
+ finally {
1559
+ session.lambdaDepth--;
1560
+ }
1561
+ }
1562
+ function evaluateMAP(args, ctx, session) {
1563
+ if (args.length < 2) {
1564
+ return ERRORS.VALUE;
1565
+ }
1566
+ const arrVal = evalDeref(args[0], ctx, session);
1567
+ const lambdaVal = evalDeref(args[args.length - 1], ctx, session);
1568
+ if (!isLambda(lambdaVal)) {
1569
+ return ERRORS.VALUE;
1570
+ }
1571
+ if (arrVal.kind !== RVKind.Array) {
1572
+ return invokeLambda(lambdaVal, [arrVal], ctx, session);
1573
+ }
1574
+ const rows = [];
1575
+ for (const row of arrVal.rows) {
1576
+ const outRow = [];
1577
+ for (const cell of row) {
1578
+ outRow.push(topLeft(invokeLambda(lambdaVal, [cell], ctx, session)));
1579
+ }
1580
+ rows.push(outRow);
1581
+ }
1582
+ return rvArray(rows);
1583
+ }
1584
+ function evaluateREDUCE(args, ctx, session) {
1585
+ if (args.length < 3) {
1586
+ return ERRORS.VALUE;
1587
+ }
1588
+ let acc = evalDeref(args[0], ctx, session);
1589
+ const arrVal = evalDeref(args[1], ctx, session);
1590
+ const lambdaVal = evalDeref(args[2], ctx, session);
1591
+ if (!isLambda(lambdaVal)) {
1592
+ return ERRORS.VALUE;
1593
+ }
1594
+ if (arrVal.kind === RVKind.Array) {
1595
+ for (const row of arrVal.rows) {
1596
+ for (const cell of row) {
1597
+ acc = invokeLambda(lambdaVal, [topLeft(acc), cell], ctx, session);
1598
+ }
1599
+ }
1600
+ }
1601
+ else {
1602
+ // Scalar input: Excel treats the scalar as a 1×1 array and invokes the
1603
+ // reducer exactly once. Previously we returned `init` unchanged, which
1604
+ // silently broke `REDUCE(0, some_scalar, lambda)` callers.
1605
+ acc = invokeLambda(lambdaVal, [topLeft(acc), topLeft(arrVal)], ctx, session);
1606
+ }
1607
+ return acc;
1608
+ }
1609
+ function evaluateSCAN(args, ctx, session) {
1610
+ if (args.length < 3) {
1611
+ return ERRORS.VALUE;
1612
+ }
1613
+ let acc = evalDeref(args[0], ctx, session);
1614
+ const arrVal = evalDeref(args[1], ctx, session);
1615
+ const lambdaVal = evalDeref(args[2], ctx, session);
1616
+ if (!isLambda(lambdaVal)) {
1617
+ return ERRORS.VALUE;
1618
+ }
1619
+ const rows = [];
1620
+ if (arrVal.kind === RVKind.Array) {
1621
+ for (const row of arrVal.rows) {
1622
+ const outRow = [];
1623
+ for (const cell of row) {
1624
+ acc = invokeLambda(lambdaVal, [topLeft(acc), cell], ctx, session);
1625
+ outRow.push(topLeft(acc));
1626
+ }
1627
+ rows.push(outRow);
1628
+ }
1629
+ return rows.length > 0 ? rvArray(rows) : ERRORS.CALC;
1630
+ }
1631
+ // Scalar input: emit a single-cell array containing the one accumulated
1632
+ // value. Previously we returned #CALC! here, which was an artefact of the
1633
+ // array-only implementation path.
1634
+ const result = invokeLambda(lambdaVal, [topLeft(acc), topLeft(arrVal)], ctx, session);
1635
+ return rvArray([[topLeft(result)]]);
1636
+ }
1637
+ function evaluateMAKEARRAY(args, ctx, session) {
1638
+ if (args.length < 3) {
1639
+ return ERRORS.VALUE;
1640
+ }
1641
+ const rowsNum = toNumberRV(topLeft(evalDeref(args[0], ctx, session)));
1642
+ if (isError(rowsNum)) {
1643
+ return rowsNum;
1644
+ }
1645
+ const colsNum = toNumberRV(topLeft(evalDeref(args[1], ctx, session)));
1646
+ if (isError(colsNum)) {
1647
+ return colsNum;
1648
+ }
1649
+ const lambdaVal = evalDeref(args[2], ctx, session);
1650
+ if (!isLambda(lambdaVal)) {
1651
+ return ERRORS.VALUE;
1652
+ }
1653
+ // Truncate toward zero and reject non-positive / overflow sizes.
1654
+ // Without the cell-count cap the engine can silently allocate billions
1655
+ // of scalars before blowing the heap; matching the broadcast limit
1656
+ // keeps MAKEARRAY in line with the rest of the array pipeline.
1657
+ const rCount = Math.trunc(rowsNum.value);
1658
+ const cCount = Math.trunc(colsNum.value);
1659
+ if (rCount < 1 || cCount < 1) {
1660
+ return ERRORS.VALUE;
1661
+ }
1662
+ if (rCount * cCount > 10000000) {
1663
+ return ERRORS.NUM;
1664
+ }
1665
+ const rows = [];
1666
+ for (let r = 1; r <= rCount; r++) {
1667
+ const outRow = [];
1668
+ for (let c = 1; c <= cCount; c++) {
1669
+ outRow.push(topLeft(invokeLambda(lambdaVal, [rvNumber(r), rvNumber(c)], ctx, session)));
1670
+ }
1671
+ rows.push(outRow);
1672
+ }
1673
+ return rvArray(rows);
1674
+ }
1675
+ function evaluateBYROW(args, ctx, session) {
1676
+ if (args.length < 2) {
1677
+ return ERRORS.VALUE;
1678
+ }
1679
+ const arrVal = evalDeref(args[0], ctx, session);
1680
+ const lambdaVal = evalDeref(args[1], ctx, session);
1681
+ if (!isLambda(lambdaVal)) {
1682
+ return ERRORS.VALUE;
1683
+ }
1684
+ if (arrVal.kind !== RVKind.Array) {
1685
+ return invokeLambda(lambdaVal, [rvArray([[topLeft(arrVal)]])], ctx, session);
1686
+ }
1687
+ const rows = [];
1688
+ for (const row of arrVal.rows) {
1689
+ const rowArr = rvArray([row.map(c => c)]);
1690
+ rows.push([topLeft(invokeLambda(lambdaVal, [rowArr], ctx, session))]);
1691
+ }
1692
+ return rvArray(rows);
1693
+ }
1694
+ function evaluateBYCOL(args, ctx, session) {
1695
+ if (args.length < 2) {
1696
+ return ERRORS.VALUE;
1697
+ }
1698
+ const arrVal = evalDeref(args[0], ctx, session);
1699
+ const lambdaVal = evalDeref(args[1], ctx, session);
1700
+ if (!isLambda(lambdaVal)) {
1701
+ return ERRORS.VALUE;
1702
+ }
1703
+ if (arrVal.kind !== RVKind.Array) {
1704
+ return invokeLambda(lambdaVal, [rvArray([[topLeft(arrVal)]])], ctx, session);
1705
+ }
1706
+ const numCols = arrVal.width;
1707
+ const outRow = [];
1708
+ for (let c = 0; c < numCols; c++) {
1709
+ const colArr = rvArray(arrVal.rows.map(row => [row[c]]));
1710
+ outRow.push(topLeft(invokeLambda(lambdaVal, [colArr], ctx, session)));
1711
+ }
1712
+ return rvArray([outRow]);
1713
+ }
1714
+ // ============================================================================
1715
+ // Reference Functions (ROW, COLUMN, ROWS, COLUMNS)
1716
+ // ============================================================================
1717
+ /** Whether `name` is one of the four "inspect a reference's geometry" builtins. */
1718
+ function isSimpleRefFunction(name) {
1719
+ return name === "ROW" || name === "COLUMN" || name === "ROWS" || name === "COLUMNS";
1720
+ }
1721
+ function tryEvaluateRefFunction(name, args, ctx) {
1722
+ switch (name) {
1723
+ case "ROW":
1724
+ if (args.length === 0) {
1725
+ return ctx.currentAddress ? rvNumber(ctx.currentAddress.row) : ERRORS.VALUE;
1726
+ }
1727
+ if (args[0].kind === BoundExprKind.CellRef) {
1728
+ return rvNumber(args[0].row);
1729
+ }
1730
+ if (args[0].kind === BoundExprKind.AreaRef) {
1731
+ return rvNumber(args[0].top);
1732
+ }
1733
+ return undefined;
1734
+ case "COLUMN":
1735
+ if (args.length === 0) {
1736
+ return ctx.currentAddress ? rvNumber(ctx.currentAddress.col) : ERRORS.VALUE;
1737
+ }
1738
+ if (args[0].kind === BoundExprKind.CellRef) {
1739
+ return rvNumber(args[0].col);
1740
+ }
1741
+ if (args[0].kind === BoundExprKind.AreaRef) {
1742
+ return rvNumber(args[0].left);
1743
+ }
1744
+ return undefined;
1745
+ case "ROWS":
1746
+ if (args.length > 0 && args[0].kind === BoundExprKind.AreaRef) {
1747
+ return rvNumber(args[0].bottom - args[0].top + 1);
1748
+ }
1749
+ if (args.length > 0 && args[0].kind === BoundExprKind.CellRef) {
1750
+ return rvNumber(1);
1751
+ }
1752
+ return undefined;
1753
+ case "COLUMNS":
1754
+ if (args.length > 0 && args[0].kind === BoundExprKind.AreaRef) {
1755
+ return rvNumber(args[0].right - args[0].left + 1);
1756
+ }
1757
+ if (args.length > 0 && args[0].kind === BoundExprKind.CellRef) {
1758
+ return rvNumber(1);
1759
+ }
1760
+ return undefined;
1761
+ default:
1762
+ return undefined;
1763
+ }
1764
+ }
1765
+ // ============================================================================
1766
+ // Reference-aware: ISREF
1767
+ // ============================================================================
1768
+ /**
1769
+ * ISREF(value) → TRUE if `value` is a reference; FALSE otherwise.
1770
+ *
1771
+ * Excel's rule is syntactic + runtime: any `CellRef` / `AreaRef` / 3-D ref /
1772
+ * `ColRangeRef` / `RowRangeRef` is a reference, and any call that *produces*
1773
+ * a `ReferenceValue` (INDIRECT, OFFSET) is also a reference. Errors in the
1774
+ * sub-expression are suppressed (Excel returns FALSE for `ISREF(INDIRECT("xx"))`
1775
+ * where INDIRECT returns `#REF!`).
1776
+ */
1777
+ function evaluateISREF(args, ctx, session) {
1778
+ if (args.length !== 1) {
1779
+ return ERRORS.VALUE;
1780
+ }
1781
+ const arg = args[0];
1782
+ // Purely syntactic reference forms — always TRUE without evaluating.
1783
+ if (arg.kind === BoundExprKind.CellRef ||
1784
+ arg.kind === BoundExprKind.AreaRef ||
1785
+ arg.kind === BoundExprKind.ColRangeRef ||
1786
+ arg.kind === BoundExprKind.RowRangeRef ||
1787
+ arg.kind === BoundExprKind.Ref3D) {
1788
+ return rvBoolean(true);
1789
+ }
1790
+ // Otherwise evaluate without dereferencing. INDIRECT/OFFSET yield a
1791
+ // ReferenceValue when successful; anything else (error, scalar, array)
1792
+ // is not a reference. Per Excel, ISREF suppresses errors to FALSE.
1793
+ const raw = evaluate(arg, ctx, session);
1794
+ return rvBoolean(raw.kind === RVKind.Reference);
1795
+ }
1796
+ // ============================================================================
1797
+ // Reference-aware: CELL
1798
+ // ============================================================================
1799
+ /**
1800
+ * Resolve a CELL(..., ref) argument to a concrete {sheet,row,col} triple.
1801
+ * CELL always inspects the *top-left* cell of the referenced area.
1802
+ * Returns an error value if the argument cannot be resolved to a reference.
1803
+ */
1804
+ function resolveCellRefArg(arg, ctx, session) {
1805
+ // Syntactic reference forms — extract top-left directly.
1806
+ if (arg.kind === BoundExprKind.CellRef) {
1807
+ return { sheet: arg.sheet, row: arg.row, col: arg.col };
1808
+ }
1809
+ if (arg.kind === BoundExprKind.AreaRef) {
1810
+ return { sheet: arg.sheet, row: arg.top, col: arg.left };
1811
+ }
1812
+ if (arg.kind === BoundExprKind.ColRangeRef) {
1813
+ const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
1814
+ const top = ws?.dimensions?.top ?? 1;
1815
+ return { sheet: arg.sheet, row: top, col: arg.leftCol };
1816
+ }
1817
+ if (arg.kind === BoundExprKind.RowRangeRef) {
1818
+ const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
1819
+ const left = ws?.dimensions?.left ?? 1;
1820
+ return { sheet: arg.sheet, row: arg.topRow, col: left };
1821
+ }
1822
+ if (arg.kind === BoundExprKind.Ref3D) {
1823
+ const first = arg.sheets[0];
1824
+ if (first === undefined) {
1825
+ return ERRORS.VALUE;
1826
+ }
1827
+ if (arg.inner.kind === BoundExprKind.CellRef) {
1828
+ return { sheet: first, row: arg.inner.row, col: arg.inner.col };
1829
+ }
1830
+ return { sheet: first, row: arg.inner.top, col: arg.inner.left };
1831
+ }
1832
+ // Fall back to evaluating — INDIRECT/OFFSET etc. may produce a ReferenceValue.
1833
+ const raw = evaluate(arg, ctx, session);
1834
+ if (raw.kind === RVKind.Error) {
1835
+ return raw;
1836
+ }
1837
+ if (raw.kind === RVKind.Reference && raw.areas.length > 0) {
1838
+ const area = raw.areas[0];
1839
+ return { sheet: area.sheet, row: area.top, col: area.left };
1840
+ }
1841
+ // Non-reference argument — Excel returns #VALUE! for CELL.
1842
+ return ERRORS.VALUE;
1843
+ }
1844
+ /** Convert a 1-based column number to its letter form (1 → "A", 27 → "AA"). */
1845
+ function colNumberToLetter(colNum) {
1846
+ let col = "";
1847
+ let cv = colNum;
1848
+ while (cv > 0) {
1849
+ cv--;
1850
+ col = String.fromCharCode(65 + (cv % 26)) + col;
1851
+ cv = Math.floor(cv / 26);
1852
+ }
1853
+ return col;
1854
+ }
1855
+ /**
1856
+ * CELL(info_type, [reference]) — limited, workbook-internal subset.
1857
+ *
1858
+ * Supported info types:
1859
+ * - `"address"` → "$A$1"-style absolute reference (no sheet name)
1860
+ * - `"row"` → 1-based row number of the top-left cell
1861
+ * - `"col"` → 1-based column number of the top-left cell
1862
+ * - `"contents"` → value of the top-left cell
1863
+ * - `"type"` → "b" (blank), "l" (label/text), "v" (value/other)
1864
+ * - `"width"` → 8 (column width is not tracked in the snapshot)
1865
+ * - `"filename"` → "" (no file path available)
1866
+ *
1867
+ * Any other info type yields `#N/A`, matching Excel's treatment of
1868
+ * workbook-state-dependent info in contexts where the data is unavailable.
1869
+ *
1870
+ * When `reference` is omitted, the current formula's own cell is used —
1871
+ * if that cannot be determined, `#VALUE!` is returned.
1872
+ */
1873
+ function evaluateCELL(args, ctx, session) {
1874
+ if (args.length < 1 || args.length > 2) {
1875
+ return ERRORS.VALUE;
1876
+ }
1877
+ // Resolve info_type — this is a plain string expression, evaluate normally.
1878
+ const infoRV = dereferenceValue(evaluate(args[0], ctx, session), ctx, session);
1879
+ if (infoRV.kind === RVKind.Error) {
1880
+ return infoRV;
1881
+ }
1882
+ const infoScalar = topLeft(infoRV);
1883
+ if (infoScalar.kind === RVKind.Error) {
1884
+ return infoScalar;
1885
+ }
1886
+ if (infoScalar.kind !== RVKind.String) {
1887
+ return ERRORS.VALUE;
1888
+ }
1889
+ const info = infoScalar.value.toLowerCase();
1890
+ // Resolve reference: explicit arg, or the current formula cell.
1891
+ let target;
1892
+ if (args.length === 2) {
1893
+ const resolved = resolveCellRefArg(args[1], ctx, session);
1894
+ if ("kind" in resolved) {
1895
+ return resolved;
1896
+ }
1897
+ target = resolved;
1898
+ }
1899
+ else {
1900
+ if (!ctx.currentAddress) {
1901
+ return ERRORS.VALUE;
1902
+ }
1903
+ target = {
1904
+ sheet: ctx.currentSheet,
1905
+ row: ctx.currentAddress.row,
1906
+ col: ctx.currentAddress.col
1907
+ };
1908
+ }
1909
+ switch (info) {
1910
+ case "address": {
1911
+ return rvString(`$${colNumberToLetter(target.col)}$${target.row}`);
1912
+ }
1913
+ case "row":
1914
+ return rvNumber(target.row);
1915
+ case "col":
1916
+ case "column":
1917
+ return rvNumber(target.col);
1918
+ case "contents": {
1919
+ return getCellValue(target.sheet, target.row, target.col, ctx, session);
1920
+ }
1921
+ case "type": {
1922
+ const val = getCellValue(target.sheet, target.row, target.col, ctx, session);
1923
+ if (val.kind === RVKind.Blank) {
1924
+ return rvString("b");
1925
+ }
1926
+ if (val.kind === RVKind.String) {
1927
+ return rvString("l");
1928
+ }
1929
+ // Numbers, booleans, errors — all classified as "value".
1930
+ return rvString("v");
1931
+ }
1932
+ case "width":
1933
+ // Column width is not captured in the snapshot — return Excel's default.
1934
+ return rvNumber(8);
1935
+ case "filename":
1936
+ // No file path is available to the calculation engine.
1937
+ return rvString("");
1938
+ default:
1939
+ return ERRORS.NA;
1940
+ }
1941
+ }
1942
+ // ============================================================================
1943
+ // Name Expression
1944
+ // ============================================================================
1945
+ function evaluateNameExpr(expr, ctx, session) {
1946
+ // Check local bindings first (LET variables)
1947
+ if (ctx.localBindings?.has(expr.upperName)) {
1948
+ return ctx.localBindings.get(expr.upperName);
1949
+ }
1950
+ // Try snapshot defined name resolution (for formula-based names).
1951
+ // Respects scope precedence: sheet-local > workbook-global.
1952
+ const dn = resolveDefinedNameFromSnapshot(ctx.snapshot.definedNames, expr.name, ctx.currentSheet);
1953
+ if (dn && dn.ranges.length > 0) {
1954
+ if (dn.ranges.length > 1) {
1955
+ return ERRORS.VALUE;
1956
+ }
1957
+ const rangeStr = dn.ranges[0];
1958
+ const parsed = parseDefinedNameRange(rangeStr);
1959
+ if (parsed) {
1960
+ if (parsed.startRow === parsed.endRow && parsed.startCol === parsed.endCol) {
1961
+ return getCellValue(parsed.sheet, parsed.startRow, parsed.startCol, ctx, session);
1962
+ }
1963
+ return buildRangeArray(ctx, session, parsed.sheet, Math.min(parsed.startRow, parsed.endRow), Math.min(parsed.startCol, parsed.endCol), Math.max(parsed.startRow, parsed.endRow), Math.max(parsed.startCol, parsed.endCol));
1964
+ }
1965
+ // Formula expression — parse and evaluate via shared helper
1966
+ const nameResult = evaluateFormulaName(expr.upperName, rangeStr, ctx, session);
1967
+ return nameResult ?? ERRORS.NAME;
1968
+ }
1969
+ return ERRORS.NAME;
1970
+ }
1971
+ // ============================================================================
1972
+ // Formula-based Defined Name Evaluation (shared helper)
1973
+ // ============================================================================
1974
+ /**
1975
+ * Evaluate a formula-based defined name expression, with caching.
1976
+ * Cache key includes name + sheet + cell address to handle position-dependent
1977
+ * formulas like ROW()/COLUMN().
1978
+ *
1979
+ * Returns the evaluated result, or undefined if parsing/evaluation fails.
1980
+ */
1981
+ function evaluateFormulaName(upperName, formulaExpr, ctx, session) {
1982
+ const addr = ctx.currentAddress;
1983
+ const cacheKey = addr
1984
+ ? `__NAME__${upperName}__${ctx.currentSheet}__${addr.row}:${addr.col}`
1985
+ : `__NAME__${upperName}__${ctx.currentSheet}`;
1986
+ const cached = session.nameCache.get(cacheKey);
1987
+ if (cached !== undefined) {
1988
+ return cached;
1989
+ }
1990
+ // Guard against recursion through a defined name that references itself.
1991
+ // Uses a dedicated prefix so it cannot collide with formula-cell guard keys.
1992
+ const guardKey = `__NAMEEVAL__${upperName}`;
1993
+ if (session.evaluating.has(guardKey)) {
1994
+ return ERRORS.CALC;
1995
+ }
1996
+ session.evaluating.add(guardKey);
1997
+ try {
1998
+ const tokens = tokenize(formulaExpr);
1999
+ const ast = parse(tokens);
2000
+ const bindCtx = { snapshot: ctx.snapshot, currentSheet: ctx.currentSheet };
2001
+ const bound = bind(ast, bindCtx);
2002
+ const result = evaluate(bound, ctx, session);
2003
+ session.nameCache.set(cacheKey, result);
2004
+ return result;
2005
+ }
2006
+ catch {
2007
+ return undefined;
2008
+ }
2009
+ finally {
2010
+ session.evaluating.delete(guardKey);
2011
+ }
2012
+ }
2013
+ // ============================================================================
2014
+ // Lambda Expression
2015
+ // ============================================================================
2016
+ function evaluateLambdaExpr(expr, ctx) {
2017
+ return rvLambda([...expr.params], expr.body, ctx.localBindings ? new Map(ctx.localBindings) : undefined);
2018
+ }
2019
+ // ============================================================================
2020
+ // Structured Reference (runtime resolution)
2021
+ // ============================================================================
2022
+ function evaluateStructuredRef(expr, ctx, session) {
2023
+ const snapshot = ctx.snapshot;
2024
+ const addr = ctx.currentAddress;
2025
+ // Find the table
2026
+ let tableName = expr.tableName;
2027
+ let tableSheet = null;
2028
+ let tableInfo = null;
2029
+ if (tableName === "") {
2030
+ // Implicit table — find the table containing the current cell
2031
+ if (!addr) {
2032
+ return ERRORS.REF;
2033
+ }
2034
+ // Sheet names are case-insensitive in Excel. Comparing the literal
2035
+ // `ws.name !== addr.sheet` would miss tables when the formula-cell's
2036
+ // address records its sheet in a different case than the workbook's
2037
+ // canonical name (possible after rename / import flows).
2038
+ const addrSheetLower = addr.sheet.toLowerCase();
2039
+ for (const ws of snapshot.worksheets) {
2040
+ if (ws.name.toLowerCase() !== addrSheetLower) {
2041
+ continue;
2042
+ }
2043
+ for (const t of ws.tables) {
2044
+ const g = buildTableGeometry(t);
2045
+ const width = t.columns.length;
2046
+ if (addr.row >= g.dataRowStart &&
2047
+ addr.row <= g.dataRowEnd &&
2048
+ addr.col >= t.topLeft.col &&
2049
+ addr.col < t.topLeft.col + width) {
2050
+ tableInfo = t;
2051
+ tableSheet = ws.name;
2052
+ tableName = t.name;
2053
+ break;
2054
+ }
2055
+ }
2056
+ if (tableInfo) {
2057
+ break;
2058
+ }
2059
+ }
2060
+ }
2061
+ else {
2062
+ // Named table — use the pre-built index for O(1) lookup
2063
+ const resolved = snapshot.tablesByName.get(tableName.toLowerCase());
2064
+ if (resolved) {
2065
+ tableInfo = resolved.table;
2066
+ tableSheet = resolved.sheetName;
2067
+ }
2068
+ }
2069
+ if (!tableInfo || !tableSheet) {
2070
+ return ERRORS.REF;
2071
+ }
2072
+ const geo = buildTableGeometry(tableInfo);
2073
+ // Strict column resolution — unknown column names surface as #REF!
2074
+ const colRange = resolveStructuredRefColumns(expr.columns, tableInfo, "strict");
2075
+ if (colRange === "error") {
2076
+ return ERRORS.REF;
2077
+ }
2078
+ const rowRange = resolveStructuredRefRows(expr.specials, geo);
2079
+ let rowTop;
2080
+ let rowBottom;
2081
+ if (rowRange === "error") {
2082
+ return ERRORS.REF;
2083
+ }
2084
+ else if (rowRange === "thisRow") {
2085
+ if (addr) {
2086
+ rowTop = addr.row;
2087
+ rowBottom = addr.row;
2088
+ }
2089
+ else {
2090
+ return ERRORS.VALUE;
2091
+ }
2092
+ }
2093
+ else {
2094
+ rowTop = rowRange.rowTop;
2095
+ rowBottom = rowRange.rowBottom;
2096
+ }
2097
+ // Single cell — return as single-cell ReferenceValue
2098
+ if (rowTop === rowBottom && colRange.colLeft === colRange.colRight) {
2099
+ return rvCellRef(tableSheet, rowTop, colRange.colLeft);
2100
+ }
2101
+ // Range — return as area ReferenceValue
2102
+ return rvRef(tableSheet, rowTop, colRange.colLeft, rowBottom, colRange.colRight);
2103
+ }
2104
+ // ============================================================================
2105
+ // Array Literal
2106
+ // ============================================================================
2107
+ function evaluateArrayLiteral(expr, ctx, session) {
2108
+ const rows = [];
2109
+ for (const row of expr.rows) {
2110
+ const evalRow = [];
2111
+ for (const elem of row) {
2112
+ evalRow.push(topLeft(evalDeref(elem, ctx, session)));
2113
+ }
2114
+ rows.push(evalRow);
2115
+ }
2116
+ return rvArray(rows);
2117
+ }
2118
+ // ============================================================================
2119
+ // Implicit Intersection
2120
+ // ============================================================================
2121
+ export function implicitIntersect(val, ctx) {
2122
+ if (isScalar(val)) {
2123
+ return val;
2124
+ }
2125
+ if (val.kind === RVKind.Lambda) {
2126
+ return val;
2127
+ }
2128
+ if (val.kind === RVKind.Reference) {
2129
+ // Implicit intersection on a reference: resolve using formula cell's row/col
2130
+ if (val.areas.length === 0) {
2131
+ return BLANK;
2132
+ }
2133
+ const area = val.areas[0];
2134
+ const isSingleCell = area.top === area.bottom && area.left === area.right;
2135
+ if (isSingleCell) {
2136
+ return val; // Single cell ref — keep as-is, will be dereferenced at use site
2137
+ }
2138
+ // Multi-cell reference — apply implicit intersection
2139
+ const addr = ctx.currentAddress;
2140
+ if (!addr) {
2141
+ return val;
2142
+ }
2143
+ // Single column — pick row
2144
+ if (area.left === area.right && addr.row >= area.top && addr.row <= area.bottom) {
2145
+ return rvCellRef(area.sheet, addr.row, area.left);
2146
+ }
2147
+ // Single row — pick column
2148
+ if (area.top === area.bottom && addr.col >= area.left && addr.col <= area.right) {
2149
+ return rvCellRef(area.sheet, area.top, addr.col);
2150
+ }
2151
+ // Both row and col
2152
+ if (addr.row >= area.top &&
2153
+ addr.row <= area.bottom &&
2154
+ addr.col >= area.left &&
2155
+ addr.col <= area.right) {
2156
+ return rvCellRef(area.sheet, addr.row, addr.col);
2157
+ }
2158
+ return val;
2159
+ }
2160
+ if (val.kind !== RVKind.Array) {
2161
+ return val;
2162
+ }
2163
+ const arr = val;
2164
+ if (arr.height === 0 || arr.width === 0) {
2165
+ return BLANK;
2166
+ }
2167
+ if (arr.height === 1 && arr.width === 1) {
2168
+ return arr.rows[0][0];
2169
+ }
2170
+ const addr = ctx.currentAddress;
2171
+ if (!addr) {
2172
+ return arr.rows[0][0];
2173
+ }
2174
+ // Single row — pick column by offset
2175
+ if (arr.height === 1) {
2176
+ if (arr.originCol !== undefined) {
2177
+ const colIdx = addr.col - arr.originCol;
2178
+ if (colIdx >= 0 && colIdx < arr.width) {
2179
+ return arr.rows[0][colIdx];
2180
+ }
2181
+ }
2182
+ return arr.rows[0][0];
2183
+ }
2184
+ // Single column — pick row by offset
2185
+ if (arr.width === 1) {
2186
+ if (arr.originRow !== undefined) {
2187
+ const rowIdx = addr.row - arr.originRow;
2188
+ if (rowIdx >= 0 && rowIdx < arr.height) {
2189
+ return arr.rows[rowIdx][0];
2190
+ }
2191
+ }
2192
+ return arr.rows[0][0];
2193
+ }
2194
+ // Multi-row, multi-column
2195
+ if (arr.originRow !== undefined && arr.originCol !== undefined) {
2196
+ const rowIdx = addr.row - arr.originRow;
2197
+ const colIdx = addr.col - arr.originCol;
2198
+ if (rowIdx >= 0 && rowIdx < arr.height && colIdx >= 0 && colIdx < arr.width) {
2199
+ return arr.rows[rowIdx][colIdx];
2200
+ }
2201
+ }
2202
+ return arr.rows[0][0];
2203
+ }
2204
+ // ============================================================================
2205
+ // R1C1 Reference Resolution
2206
+ // ============================================================================
2207
+ function resolveR1C1(refText, ctx, session) {
2208
+ const upper = refText.toUpperCase().trim();
2209
+ // Check for range separator
2210
+ let depth = 0;
2211
+ let sepIdx = -1;
2212
+ for (let i = 0; i < upper.length; i++) {
2213
+ if (upper[i] === "[") {
2214
+ depth++;
2215
+ }
2216
+ else if (upper[i] === "]") {
2217
+ depth--;
2218
+ }
2219
+ else if (upper[i] === ":" && depth === 0) {
2220
+ sepIdx = i;
2221
+ break;
2222
+ }
2223
+ }
2224
+ if (sepIdx !== -1) {
2225
+ const s = parseR1C1Single(upper.slice(0, sepIdx), ctx);
2226
+ const e = parseR1C1Single(upper.slice(sepIdx + 1), ctx);
2227
+ if (!s || !e) {
2228
+ return ERRORS.REF;
2229
+ }
2230
+ const top = Math.min(s.row, e.row);
2231
+ const bottom = Math.max(s.row, e.row);
2232
+ const left = Math.min(s.col, e.col);
2233
+ const right = Math.max(s.col, e.col);
2234
+ return rvRef(ctx.currentSheet, top, left, bottom, right);
2235
+ }
2236
+ const ref = parseR1C1Single(upper, ctx);
2237
+ if (!ref || ref.row < 1 || ref.col < 1) {
2238
+ return ERRORS.REF;
2239
+ }
2240
+ return rvCellRef(ctx.currentSheet, ref.row, ref.col);
2241
+ }
2242
+ function parseR1C1Single(text, ctx) {
2243
+ const re = /^R(\[(-?\d+)\]|(\d+))C(\[(-?\d+)\]|(\d+))$/;
2244
+ const m = re.exec(text);
2245
+ if (!m) {
2246
+ return null;
2247
+ }
2248
+ const addr = ctx.currentAddress;
2249
+ const row = m[2] !== undefined ? (addr?.row ?? 1) + parseInt(m[2], 10) : parseInt(m[3], 10);
2250
+ const col = m[5] !== undefined ? (addr?.col ?? 1) + parseInt(m[5], 10) : parseInt(m[6], 10);
2251
+ return { row, col };
2252
+ }
2253
+ // ============================================================================
2254
+ // Helpers
2255
+ // ============================================================================
2256
+ /**
2257
+ * Evaluate a BoundExpr and dereference any resulting ReferenceValue.
2258
+ * Use this whenever a concrete (non-reference) value is needed.
2259
+ */
2260
+ function evalDeref(expr, ctx, session) {
2261
+ return dereferenceValue(evaluate(expr, ctx, session), ctx, session);
2262
+ }
2263
+ function resolveLambdaName(name, args, ctx, session) {
2264
+ // Check local bindings
2265
+ if (ctx.localBindings?.has(name)) {
2266
+ const val = ctx.localBindings.get(name);
2267
+ if (isLambda(val)) {
2268
+ return invokeLambda(val, args, ctx, session);
2269
+ }
2270
+ }
2271
+ // Check defined names that resolve to lambdas (via snapshot, scope-aware)
2272
+ const dn = resolveDefinedNameFromSnapshot(ctx.snapshot.definedNames, name, ctx.currentSheet);
2273
+ if (dn && dn.ranges.length === 1) {
2274
+ const rangeStr = dn.ranges[0];
2275
+ const parsed = parseDefinedNameRange(rangeStr);
2276
+ if (parsed && parsed.startRow === parsed.endRow && parsed.startCol === parsed.endCol) {
2277
+ const cellVal = getCellValue(parsed.sheet, parsed.startRow, parsed.startCol, ctx, session);
2278
+ if (isLambda(cellVal)) {
2279
+ return invokeLambda(cellVal, args, ctx, session);
2280
+ }
2281
+ }
2282
+ // Formula-based name — evaluate via shared helper
2283
+ if (!parsed) {
2284
+ const nameVal = evaluateFormulaName(name.toUpperCase(), rangeStr, ctx, session);
2285
+ if (nameVal !== undefined && isLambda(nameVal)) {
2286
+ return invokeLambda(nameVal, args, ctx, session);
2287
+ }
2288
+ }
2289
+ }
2290
+ return undefined;
2291
+ }