@cj-tech-master/excelts 9.3.1 → 9.4.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.
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/modules/excel/cell.d.ts +18 -0
- package/dist/browser/modules/excel/cell.js +21 -0
- package/dist/browser/modules/excel/utils/cell-format.js +85 -13
- package/dist/browser/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/browser/modules/excel/workbook.browser.js +49 -0
- package/dist/browser/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/browser/modules/formula/compile/binder.js +48 -6
- package/dist/browser/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/browser/modules/formula/compile/bound-ast.js +1 -0
- package/dist/browser/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/browser/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/browser/modules/formula/functions/_shared.js +47 -0
- package/dist/browser/modules/formula/functions/conditional.js +103 -22
- package/dist/browser/modules/formula/functions/date.js +105 -23
- package/dist/browser/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/browser/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/browser/modules/formula/functions/engineering.js +103 -151
- package/dist/browser/modules/formula/functions/financial.js +210 -184
- package/dist/browser/modules/formula/functions/lookup.js +224 -157
- package/dist/browser/modules/formula/functions/math.d.ts +26 -0
- package/dist/browser/modules/formula/functions/math.js +249 -69
- package/dist/browser/modules/formula/functions/statistical.js +221 -171
- package/dist/browser/modules/formula/functions/text.js +112 -52
- package/dist/browser/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/browser/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/browser/modules/formula/materialize/types.d.ts +15 -0
- package/dist/browser/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/browser/modules/formula/runtime/evaluator.js +582 -162
- package/dist/browser/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/browser/modules/formula/runtime/function-registry.js +59 -13
- package/dist/browser/modules/formula/runtime/values.d.ts +13 -0
- package/dist/browser/modules/formula/runtime/values.js +20 -2
- package/dist/browser/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/browser/modules/formula/syntax/ast.js +1 -0
- package/dist/browser/modules/formula/syntax/parser.js +29 -7
- package/dist/browser/modules/formula/syntax/token-types.d.ts +4 -0
- package/dist/browser/modules/formula/syntax/token-types.js +9 -0
- package/dist/browser/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/modules/excel/cell.js +21 -0
- package/dist/cjs/modules/excel/utils/cell-format.js +85 -13
- package/dist/cjs/modules/excel/workbook.browser.js +49 -0
- package/dist/cjs/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/cjs/modules/formula/compile/binder.js +48 -6
- package/dist/cjs/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/cjs/modules/formula/functions/_shared.js +48 -0
- package/dist/cjs/modules/formula/functions/conditional.js +103 -22
- package/dist/cjs/modules/formula/functions/date.js +104 -22
- package/dist/cjs/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/cjs/modules/formula/functions/engineering.js +109 -157
- package/dist/cjs/modules/formula/functions/financial.js +209 -183
- package/dist/cjs/modules/formula/functions/lookup.js +224 -157
- package/dist/cjs/modules/formula/functions/math.js +254 -70
- package/dist/cjs/modules/formula/functions/statistical.js +222 -172
- package/dist/cjs/modules/formula/functions/text.js +112 -52
- package/dist/cjs/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/cjs/modules/formula/runtime/evaluator.js +581 -161
- package/dist/cjs/modules/formula/runtime/function-registry.js +57 -11
- package/dist/cjs/modules/formula/runtime/values.js +21 -2
- package/dist/cjs/modules/formula/syntax/parser.js +29 -7
- package/dist/cjs/modules/formula/syntax/token-types.js +9 -0
- package/dist/cjs/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/esm/index.js +2 -0
- package/dist/esm/modules/excel/cell.js +21 -0
- package/dist/esm/modules/excel/utils/cell-format.js +85 -13
- package/dist/esm/modules/excel/workbook.browser.js +49 -0
- package/dist/esm/modules/excel/xlsx/defaultnumformats.js +3 -3
- package/dist/esm/modules/formula/compile/binder.js +48 -6
- package/dist/esm/modules/formula/compile/bound-ast.js +1 -0
- package/dist/esm/modules/formula/compile/compiled-formula.js +41 -8
- package/dist/esm/modules/formula/functions/_shared.js +47 -0
- package/dist/esm/modules/formula/functions/conditional.js +103 -22
- package/dist/esm/modules/formula/functions/date.js +105 -23
- package/dist/esm/modules/formula/functions/dynamic-array.js +173 -69
- package/dist/esm/modules/formula/functions/engineering.js +103 -151
- package/dist/esm/modules/formula/functions/financial.js +210 -184
- package/dist/esm/modules/formula/functions/lookup.js +224 -157
- package/dist/esm/modules/formula/functions/math.js +249 -69
- package/dist/esm/modules/formula/functions/statistical.js +221 -171
- package/dist/esm/modules/formula/functions/text.js +112 -52
- package/dist/esm/modules/formula/integration/calculate-formulas-impl.js +20 -1
- package/dist/esm/modules/formula/materialize/build-writeback-plan.js +10 -6
- package/dist/esm/modules/formula/runtime/evaluator.js +582 -162
- package/dist/esm/modules/formula/runtime/function-registry.js +59 -13
- package/dist/esm/modules/formula/runtime/values.js +20 -2
- package/dist/esm/modules/formula/syntax/ast.js +1 -0
- package/dist/esm/modules/formula/syntax/parser.js +29 -7
- package/dist/esm/modules/formula/syntax/token-types.js +9 -0
- package/dist/esm/modules/formula/syntax/tokenizer.js +76 -19
- package/dist/iife/excelts.iife.js +1502 -1379
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +26 -26
- package/dist/types/index.d.ts +1 -0
- package/dist/types/modules/excel/cell.d.ts +18 -0
- package/dist/types/modules/excel/workbook.browser.d.ts +57 -0
- package/dist/types/modules/formula/compile/bound-ast.d.ts +16 -2
- package/dist/types/modules/formula/functions/_shared.d.ts +19 -0
- package/dist/types/modules/formula/functions/engineering.d.ts +2 -2
- package/dist/types/modules/formula/functions/math.d.ts +26 -0
- package/dist/types/modules/formula/materialize/types.d.ts +15 -0
- package/dist/types/modules/formula/runtime/evaluator.d.ts +8 -0
- package/dist/types/modules/formula/runtime/function-registry.d.ts +5 -0
- package/dist/types/modules/formula/runtime/values.d.ts +13 -0
- package/dist/types/modules/formula/syntax/ast.d.ts +14 -2
- package/dist/types/modules/formula/syntax/token-types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import { parse } from "../syntax/parser.js";
|
|
|
14
14
|
import { stripFunctionPrefix } from "../syntax/token-types.js";
|
|
15
15
|
import { tokenize } from "../syntax/tokenizer.js";
|
|
16
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";
|
|
17
|
+
import { RVKind, BLANK, ERRORS, rvNumber, rvString, rvBoolean, rvError, rvArray, rvArrayRect, rvRef, rvCellRef, rvLambda, isError, isScalar, isLambda, toNumberRV, toStringRV, toBooleanRV, topLeft, scalarEquals, compareScalarsSameKind, fromSnapshotValue } from "./values.js";
|
|
18
18
|
/**
|
|
19
19
|
* Per-calculation mutable state.
|
|
20
20
|
*/
|
|
@@ -117,9 +117,13 @@ export function evaluate(expr, ctx, session) {
|
|
|
117
117
|
case BoundExprKind.Literal:
|
|
118
118
|
return evaluateLiteral(expr);
|
|
119
119
|
case BoundExprKind.CellRef:
|
|
120
|
-
|
|
120
|
+
// Inlined: the wrapper function did nothing beyond forwarding.
|
|
121
|
+
// Cell refs are the hottest AST node in the evaluator; saving one
|
|
122
|
+
// call frame per reference meaningfully shortens the trace for
|
|
123
|
+
// workbooks with tens of thousands of cells.
|
|
124
|
+
return rvCellRef(expr.sheet, expr.row, expr.col);
|
|
121
125
|
case BoundExprKind.AreaRef:
|
|
122
|
-
return
|
|
126
|
+
return rvRef(expr.sheet, expr.top, expr.left, expr.bottom, expr.right);
|
|
123
127
|
case BoundExprKind.ColRangeRef:
|
|
124
128
|
return evaluateColRange(expr, ctx, session);
|
|
125
129
|
case BoundExprKind.RowRangeRef:
|
|
@@ -144,6 +148,8 @@ export function evaluate(expr, ctx, session) {
|
|
|
144
148
|
return evaluateLambdaExpr(expr, ctx);
|
|
145
149
|
case BoundExprKind.StructuredRef:
|
|
146
150
|
return evaluateStructuredRef(expr, ctx, session);
|
|
151
|
+
case BoundExprKind.UnionRef:
|
|
152
|
+
return evaluateUnionRef(expr, ctx, session);
|
|
147
153
|
default:
|
|
148
154
|
return assertNever(expr);
|
|
149
155
|
}
|
|
@@ -172,15 +178,13 @@ function evaluateLiteral(expr) {
|
|
|
172
178
|
// ============================================================================
|
|
173
179
|
// Cell Reference
|
|
174
180
|
// ============================================================================
|
|
175
|
-
function evaluateCellRef(expr, ctx, session) {
|
|
176
|
-
return rvCellRef(expr.sheet, expr.row, expr.col);
|
|
177
|
-
}
|
|
178
181
|
// ============================================================================
|
|
179
|
-
// Area Reference
|
|
182
|
+
// Cell / Area Reference — inlined at the `evaluate` switch above.
|
|
180
183
|
// ============================================================================
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
// Single-arg wrappers used to exist here (`evaluateCellRef` / `evaluateAreaRef`);
|
|
185
|
+
// they did nothing beyond forwarding to `rvCellRef` / `rvRef`. Since the
|
|
186
|
+
// evaluator dispatches these on every cell in every formula, we inline
|
|
187
|
+
// them at the caller to shave one call frame.
|
|
184
188
|
function evaluateColRange(expr, ctx, session) {
|
|
185
189
|
const ws = ctx.snapshot.worksheetsByName.get(expr.sheet.toLowerCase());
|
|
186
190
|
if (!ws || !ws.dimensions) {
|
|
@@ -250,23 +254,47 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
250
254
|
const ws = ctx.snapshot.worksheetsByName.get(sheet.toLowerCase());
|
|
251
255
|
const cells = ws?.cells;
|
|
252
256
|
const wsHiddenRows = ws?.hiddenRows;
|
|
257
|
+
// Use the canonical sheet name for cache-key computation so that a
|
|
258
|
+
// mis-cased sheet identifier (e.g. `sheet1` when the workbook has
|
|
259
|
+
// `Sheet1`) doesn't bypass the compiled-formula / result caches. The
|
|
260
|
+
// map is keyed by snapshot.worksheets[].name, so any divergence would
|
|
261
|
+
// cause spurious re-compilation.
|
|
262
|
+
const canonicalSheet = ws?.name ?? sheet;
|
|
253
263
|
const compiledFormulas = ctx.compiledFormulas;
|
|
254
264
|
const resultCache = session.resultCache;
|
|
255
265
|
// Hoist the recording guard — when not recording, skip recordAccess
|
|
256
266
|
// entirely in the loop instead of paying the function-call overhead.
|
|
257
267
|
const recording = session.recordingKey !== null;
|
|
258
|
-
const
|
|
268
|
+
const height = bottom - top + 1;
|
|
269
|
+
const width = right - left + 1;
|
|
270
|
+
// Missing worksheet: emit an all-BLANK rectangle without entering the
|
|
271
|
+
// hot path. The tokenizer and binder usually report these as #REF! at
|
|
272
|
+
// compile time, but `evaluateRef3D` and runtime INDIRECT can still
|
|
273
|
+
// synthesise refs into non-existent sheets.
|
|
274
|
+
if (!cells) {
|
|
275
|
+
const rows = new Array(height);
|
|
276
|
+
for (let r = 0; r < height; r++) {
|
|
277
|
+
rows[r] = new Array(width).fill(BLANK);
|
|
278
|
+
}
|
|
279
|
+
if (recording) {
|
|
280
|
+
for (let r = top; r <= bottom; r++) {
|
|
281
|
+
for (let c = left; c <= right; c++) {
|
|
282
|
+
session.recordAccess(sheet, r, c);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return rvArrayRect(rows, height, width, top, left);
|
|
287
|
+
}
|
|
288
|
+
const rows = new Array(height);
|
|
259
289
|
// Lazily-allocated masks: only materialized once we encounter a
|
|
260
290
|
// SUBTOTAL/AGGREGATE cell or a hidden row inside the range. For the
|
|
261
291
|
// common case (plain data range, no hidden rows) we never touch these
|
|
262
292
|
// and emit an ArrayValue without extra metadata.
|
|
263
293
|
let subtotalMask;
|
|
264
294
|
let hiddenRowMask;
|
|
265
|
-
const height = bottom - top + 1;
|
|
266
|
-
const width = right - left + 1;
|
|
267
295
|
for (let r = top; r <= bottom; r++) {
|
|
268
|
-
const row = [];
|
|
269
296
|
const ri = r - top;
|
|
297
|
+
const row = new Array(width);
|
|
270
298
|
// Record row visibility — SUBTOTAL 1xx / AGGREGATE opt 5/7 use it.
|
|
271
299
|
if (wsHiddenRows?.has(r)) {
|
|
272
300
|
if (!hiddenRowMask) {
|
|
@@ -275,25 +303,21 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
275
303
|
hiddenRowMask[ri] = true;
|
|
276
304
|
}
|
|
277
305
|
for (let c = left; c <= right; c++) {
|
|
306
|
+
const ci = c - left;
|
|
278
307
|
if (recording) {
|
|
279
|
-
session.recordAccess(
|
|
280
|
-
}
|
|
281
|
-
// Missing worksheet: matches getCellValue's BLANK fallback.
|
|
282
|
-
if (!cells) {
|
|
283
|
-
row.push(BLANK);
|
|
284
|
-
continue;
|
|
308
|
+
session.recordAccess(canonicalSheet, r, c);
|
|
285
309
|
}
|
|
286
310
|
const cell = cells.get(snapshotCellKey(r, c));
|
|
287
311
|
if (!cell) {
|
|
288
312
|
// No snapshot cell yet — might still be inside a live spill
|
|
289
313
|
// (e.g. reading A2..A5 while A1 = SEQUENCE(5) is still being
|
|
290
314
|
// materialized). See `readLiveSpill` for the lookup path.
|
|
291
|
-
const live = readLiveSpill(
|
|
292
|
-
row
|
|
315
|
+
const live = readLiveSpill(canonicalSheet, r, c, session);
|
|
316
|
+
row[ci] = live ? topLeft(live) : BLANK;
|
|
293
317
|
continue;
|
|
294
318
|
}
|
|
295
319
|
if (cell.formulaKind !== "none" && cell.formula) {
|
|
296
|
-
const fKey = formulaCellKey(
|
|
320
|
+
const fKey = formulaCellKey(canonicalSheet, r, c);
|
|
297
321
|
const compiled = compiledFormulas.get(fKey);
|
|
298
322
|
// Mark SUBTOTAL/AGGREGATE output cells so an outer SUBTOTAL /
|
|
299
323
|
// AGGREGATE over this range knows to skip them (Excel's
|
|
@@ -305,15 +329,15 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
305
329
|
subtotalMask[i] = new Array(width).fill(false);
|
|
306
330
|
}
|
|
307
331
|
}
|
|
308
|
-
subtotalMask[ri][
|
|
332
|
+
subtotalMask[ri][ci] = true;
|
|
309
333
|
}
|
|
310
334
|
const cached = resultCache.get(fKey);
|
|
311
335
|
if (cached !== undefined) {
|
|
312
|
-
row
|
|
336
|
+
row[ci] = topLeft(cached.scalar);
|
|
313
337
|
continue;
|
|
314
338
|
}
|
|
315
339
|
if (compiled) {
|
|
316
|
-
row
|
|
340
|
+
row[ci] = topLeft(evaluateFormula(compiled, ctx, session));
|
|
317
341
|
continue;
|
|
318
342
|
}
|
|
319
343
|
}
|
|
@@ -321,16 +345,16 @@ function buildRangeArray(ctx, session, sheet, top, left, bottom, right) {
|
|
|
321
345
|
// that a fresh dynamic-array spill is about to overwrite. Prefer
|
|
322
346
|
// the live value when a master is registered so this-pass SUM /
|
|
323
347
|
// LOOKUP / etc. see the new spill immediately.
|
|
324
|
-
const live = readLiveSpill(
|
|
348
|
+
const live = readLiveSpill(canonicalSheet, r, c, session);
|
|
325
349
|
if (live) {
|
|
326
|
-
row
|
|
350
|
+
row[ci] = topLeft(live);
|
|
327
351
|
continue;
|
|
328
352
|
}
|
|
329
|
-
row
|
|
353
|
+
row[ci] = topLeft(fromSnapshotValue(cell.value));
|
|
330
354
|
}
|
|
331
|
-
rows
|
|
355
|
+
rows[ri] = row;
|
|
332
356
|
}
|
|
333
|
-
return
|
|
357
|
+
return rvArrayRect(rows, height, width, top, left, subtotalMask, hiddenRowMask);
|
|
334
358
|
}
|
|
335
359
|
// ============================================================================
|
|
336
360
|
// Dereference: Reference → concrete value
|
|
@@ -429,15 +453,25 @@ function dereferenceValue(v, ctx, session) {
|
|
|
429
453
|
// Get Cell Value from Snapshot
|
|
430
454
|
// ============================================================================
|
|
431
455
|
function getCellValue(sheetName, row, col, ctx, session) {
|
|
456
|
+
const ws = ctx.snapshot.worksheetsByName.get(sheetName.toLowerCase());
|
|
457
|
+
if (!ws) {
|
|
458
|
+
// Record the access before returning — still a valid dependency
|
|
459
|
+
// edge even if the sheet is missing (it'll surface as BLANK
|
|
460
|
+
// downstream, but an INDIRECT-produced read should still register).
|
|
461
|
+
if (session.recordingKey !== null) {
|
|
462
|
+
session.recordAccess(sheetName, row, col);
|
|
463
|
+
}
|
|
464
|
+
return BLANK;
|
|
465
|
+
}
|
|
466
|
+
// Normalise to the canonical sheet name so downstream cache keys
|
|
467
|
+
// (compiled formulas, result cache, spill map) all hit the same
|
|
468
|
+
// entries regardless of how the caller cased the input.
|
|
469
|
+
const canonicalSheet = ws.name;
|
|
432
470
|
// Record this access for runtime dependency tracking. Inline guard
|
|
433
471
|
// avoids the function call overhead in the common case where no
|
|
434
472
|
// recording is active (formulas without dynamic refs).
|
|
435
473
|
if (session.recordingKey !== null) {
|
|
436
|
-
session.recordAccess(
|
|
437
|
-
}
|
|
438
|
-
const ws = ctx.snapshot.worksheetsByName.get(sheetName.toLowerCase());
|
|
439
|
-
if (!ws) {
|
|
440
|
-
return BLANK;
|
|
474
|
+
session.recordAccess(canonicalSheet, row, col);
|
|
441
475
|
}
|
|
442
476
|
const cellKey = snapshotCellKey(row, col);
|
|
443
477
|
const cell = ws.cells.get(cellKey);
|
|
@@ -447,11 +481,18 @@ function getCellValue(sheetName, row, col, ctx, session) {
|
|
|
447
481
|
// right array element. Matters when a downstream formula like
|
|
448
482
|
// `SUM(A1:A5)` runs before materialize writes the ghost cells for
|
|
449
483
|
// `A1 = SEQUENCE(5)` into the snapshot.
|
|
450
|
-
return readLiveSpill(
|
|
484
|
+
return readLiveSpill(canonicalSheet, row, col, session) ?? BLANK;
|
|
451
485
|
}
|
|
452
486
|
// If this cell has a formula, evaluate it
|
|
453
487
|
if (cell.formulaKind !== "none" && cell.formula) {
|
|
454
|
-
|
|
488
|
+
// Use `ws.name` (the snapshot's canonical case) rather than the
|
|
489
|
+
// caller's `sheetName`, which could arrive mis-cased (e.g. the
|
|
490
|
+
// user wrote `sheet1!A1` but the workbook has `Sheet1`). The
|
|
491
|
+
// compiled-formula map is keyed by the canonical form, so a
|
|
492
|
+
// mis-cased key would produce a spurious cache-miss + re-compile
|
|
493
|
+
// per read — and worse, a different key than the one the
|
|
494
|
+
// write-back cycle will later upsert.
|
|
495
|
+
const fKey = formulaCellKey(canonicalSheet, row, col);
|
|
455
496
|
// Check cache — return scalar form for dependency resolution
|
|
456
497
|
const cached = session.resultCache.get(fKey);
|
|
457
498
|
if (cached !== undefined) {
|
|
@@ -467,7 +508,7 @@ function getCellValue(sheetName, row, col, ctx, session) {
|
|
|
467
508
|
// (e.g. a value that exists in the snapshot from a previous calc
|
|
468
509
|
// cycle but is about to be overwritten by a fresh spill). Prefer the
|
|
469
510
|
// live value when a master is registered.
|
|
470
|
-
const spill = readLiveSpill(
|
|
511
|
+
const spill = readLiveSpill(canonicalSheet, row, col, session);
|
|
471
512
|
if (spill) {
|
|
472
513
|
return spill;
|
|
473
514
|
}
|
|
@@ -791,45 +832,45 @@ function applyScalarBinaryOp(op, left, right) {
|
|
|
791
832
|
return !isFinite(result) ? ERRORS.NUM : rvNumber(result);
|
|
792
833
|
}
|
|
793
834
|
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
835
|
let cmp;
|
|
812
|
-
|
|
813
|
-
|
|
836
|
+
// Fast path — same kind, no blanks. Covers the vast majority of
|
|
837
|
+
// comparisons and avoids the blank-normalisation allocation dance
|
|
838
|
+
// below. compareScalarsSameKind handles Number/String/Boolean/Blank
|
|
839
|
+
// intrinsically; the NaN case only arises for Error kinds.
|
|
840
|
+
if (left.kind === right.kind && left.kind !== RVKind.Blank) {
|
|
841
|
+
cmp = compareScalarsSameKind(left, right);
|
|
814
842
|
if (!Number.isFinite(cmp)) {
|
|
815
843
|
cmp = 0;
|
|
816
844
|
}
|
|
817
845
|
}
|
|
818
846
|
else {
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
847
|
+
// Normalize blanks to a neutral form of the opposing kind so formulas like
|
|
848
|
+
// `"" = A1` (where A1 is blank) compare equal. Without this normalisation
|
|
849
|
+
// Excel would route us to the cross-type tiebreak below.
|
|
850
|
+
const l = left.kind === RVKind.Blank
|
|
851
|
+
? right.kind === RVKind.String
|
|
852
|
+
? rvString("")
|
|
853
|
+
: right.kind === RVKind.Boolean
|
|
854
|
+
? rvBoolean(false)
|
|
855
|
+
: rvNumber(0)
|
|
856
|
+
: left;
|
|
857
|
+
const r = right.kind === RVKind.Blank
|
|
858
|
+
? left.kind === RVKind.String
|
|
859
|
+
? rvString("")
|
|
860
|
+
: left.kind === RVKind.Boolean
|
|
861
|
+
? rvBoolean(false)
|
|
862
|
+
: rvNumber(0)
|
|
863
|
+
: right;
|
|
864
|
+
if (l.kind === r.kind) {
|
|
865
|
+
cmp = compareScalarsSameKind(l, r);
|
|
866
|
+
if (!Number.isFinite(cmp)) {
|
|
867
|
+
cmp = 0;
|
|
826
868
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
cmp = order(l) - order(r);
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
// Excel orders scalar kinds: Number < String < Boolean < Error/Blank.
|
|
872
|
+
cmp = scalarKindOrder(l) - scalarKindOrder(r);
|
|
873
|
+
}
|
|
833
874
|
}
|
|
834
875
|
switch (op) {
|
|
835
876
|
case "=":
|
|
@@ -848,6 +889,19 @@ function compareScalars(left, right, op) {
|
|
|
848
889
|
return false;
|
|
849
890
|
}
|
|
850
891
|
}
|
|
892
|
+
/** Kind-priority used by Excel when comparing scalars of different kinds. */
|
|
893
|
+
function scalarKindOrder(v) {
|
|
894
|
+
switch (v.kind) {
|
|
895
|
+
case RVKind.Number:
|
|
896
|
+
return 0;
|
|
897
|
+
case RVKind.String:
|
|
898
|
+
return 1;
|
|
899
|
+
case RVKind.Boolean:
|
|
900
|
+
return 2;
|
|
901
|
+
default:
|
|
902
|
+
return 3; // Error / Blank — callers rarely reach this branch.
|
|
903
|
+
}
|
|
904
|
+
}
|
|
851
905
|
function broadcastBinaryOp(op, left, right) {
|
|
852
906
|
const lArr = left.kind === RVKind.Array ? left : null;
|
|
853
907
|
const rArr = right.kind === RVKind.Array ? right : null;
|
|
@@ -872,28 +926,37 @@ function broadcastBinaryOp(op, left, right) {
|
|
|
872
926
|
// inner loop (outRows × outCols calls) was pure overhead.
|
|
873
927
|
const lScalarFallback = lArr ? undefined : topLeft(left);
|
|
874
928
|
const rScalarFallback = rArr ? undefined : topLeft(right);
|
|
875
|
-
|
|
929
|
+
// Hoist broadcast-flag checks outside the hot loop. The four flags are
|
|
930
|
+
// constant for the entire operation; JS engines usually fold these, but
|
|
931
|
+
// turning them into `const` locals lets the inner loop read bools rather
|
|
932
|
+
// than recomputing `=== 1` each cell.
|
|
933
|
+
const lRowBroadcast = lRows === 1;
|
|
934
|
+
const lColBroadcast = lCols === 1;
|
|
935
|
+
const rRowBroadcast = rRows === 1;
|
|
936
|
+
const rColBroadcast = rCols === 1;
|
|
937
|
+
const rows = new Array(outRows);
|
|
876
938
|
for (let r = 0; r < outRows; r++) {
|
|
877
|
-
|
|
939
|
+
// Cache the row handle once per output row — avoids `arr.rows[lR]`
|
|
940
|
+
// double-indexing per cell. When the left operand broadcasts along
|
|
941
|
+
// rows (height 1) we read the same row handle every iteration.
|
|
942
|
+
const lRow = lArr ? lArr.rows[lRowBroadcast ? 0 : r] : undefined;
|
|
943
|
+
const rRow = rArr ? rArr.rows[rRowBroadcast ? 0 : r] : undefined;
|
|
944
|
+
const outRow = new Array(outCols);
|
|
878
945
|
for (let c = 0; c < outCols; c++) {
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const rVal = rArr ? rArr.rows[rR][rC] : rScalarFallback;
|
|
889
|
-
row.push(applyScalarBinaryOp(op, lVal, rVal));
|
|
890
|
-
}
|
|
891
|
-
rows.push(row);
|
|
946
|
+
const lVal = lRow
|
|
947
|
+
? lRow[lColBroadcast ? 0 : c]
|
|
948
|
+
: lScalarFallback;
|
|
949
|
+
const rVal = rRow
|
|
950
|
+
? rRow[rColBroadcast ? 0 : c]
|
|
951
|
+
: rScalarFallback;
|
|
952
|
+
outRow[c] = applyScalarBinaryOp(op, lVal, rVal);
|
|
953
|
+
}
|
|
954
|
+
rows[r] = outRow;
|
|
892
955
|
}
|
|
893
956
|
// Propagate origin metadata
|
|
894
957
|
const originRow = lArr?.originRow ?? rArr?.originRow;
|
|
895
958
|
const originCol = lArr?.originCol ?? rArr?.originCol;
|
|
896
|
-
return
|
|
959
|
+
return rvArrayRect(rows, outRows, outCols, originRow, originCol);
|
|
897
960
|
}
|
|
898
961
|
// ============================================================================
|
|
899
962
|
// Unary Operations
|
|
@@ -985,16 +1048,43 @@ function evaluateCall(expr, ctx, session) {
|
|
|
985
1048
|
if (expr.args.length === 1 && isSimpleRefFunction(canonical)) {
|
|
986
1049
|
const raw = evaluate(expr.args[0], ctx, session);
|
|
987
1050
|
if (raw.kind === RVKind.Reference && raw.areas.length > 0) {
|
|
988
|
-
|
|
1051
|
+
// ROW / COLUMN pick the top-left coordinate of the first area —
|
|
1052
|
+
// matches Excel's reference-position rule for multi-area refs.
|
|
1053
|
+
// ROWS / COLUMNS match the flattened shape that `dereferenceValue`
|
|
1054
|
+
// would produce: for a multi-area union we stack all areas
|
|
1055
|
+
// vertically, so ROWS sums every area's height and COLUMNS takes
|
|
1056
|
+
// the max width. This keeps `ROWS(union)` consistent with
|
|
1057
|
+
// `SUMPRODUCT(–(union=union))` / `SUM(union)` — all of which see
|
|
1058
|
+
// the stacked view — instead of silently dropping the tail areas.
|
|
1059
|
+
const areas = raw.areas;
|
|
989
1060
|
switch (canonical) {
|
|
990
1061
|
case "ROW":
|
|
991
|
-
return rvNumber(
|
|
1062
|
+
return rvNumber(areas[0].top);
|
|
992
1063
|
case "COLUMN":
|
|
993
|
-
return rvNumber(
|
|
994
|
-
case "ROWS":
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1064
|
+
return rvNumber(areas[0].left);
|
|
1065
|
+
case "ROWS": {
|
|
1066
|
+
if (areas.length === 1) {
|
|
1067
|
+
return rvNumber(areas[0].bottom - areas[0].top + 1);
|
|
1068
|
+
}
|
|
1069
|
+
let total = 0;
|
|
1070
|
+
for (const a of areas) {
|
|
1071
|
+
total += a.bottom - a.top + 1;
|
|
1072
|
+
}
|
|
1073
|
+
return rvNumber(total);
|
|
1074
|
+
}
|
|
1075
|
+
case "COLUMNS": {
|
|
1076
|
+
if (areas.length === 1) {
|
|
1077
|
+
return rvNumber(areas[0].right - areas[0].left + 1);
|
|
1078
|
+
}
|
|
1079
|
+
let maxW = 0;
|
|
1080
|
+
for (const a of areas) {
|
|
1081
|
+
const w = a.right - a.left + 1;
|
|
1082
|
+
if (w > maxW) {
|
|
1083
|
+
maxW = w;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return rvNumber(maxW);
|
|
1087
|
+
}
|
|
998
1088
|
}
|
|
999
1089
|
}
|
|
1000
1090
|
}
|
|
@@ -1007,10 +1097,66 @@ function evaluateCall(expr, ctx, session) {
|
|
|
1007
1097
|
if (canonical === "CELL") {
|
|
1008
1098
|
return evaluateCELL(expr.args, ctx, session);
|
|
1009
1099
|
}
|
|
1100
|
+
// ── INDEX reference-aware path ──
|
|
1101
|
+
// Excel's INDEX takes an optional fourth `area_num` that selects
|
|
1102
|
+
// which member of a multi-area reference to index into, e.g.
|
|
1103
|
+
// `INDEX((A1:B2, D4:E5), 1, 1, 2) = D4`. We need to see the raw
|
|
1104
|
+
// ReferenceValue (not its flattened deref array) to support this.
|
|
1105
|
+
if (canonical === "INDEX") {
|
|
1106
|
+
const result = tryEvaluateINDEX(expr.args, ctx, session);
|
|
1107
|
+
if (result !== undefined) {
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// ── AREAS reference-aware path ──
|
|
1112
|
+
// `AREAS((A1:B2, D4:E5))` should return 2, not 1. The standard
|
|
1113
|
+
// eager-dereference path flattens multi-area references into a single
|
|
1114
|
+
// stacked ArrayValue, losing the area count. Intercept here so we
|
|
1115
|
+
// see the raw ReferenceValue.
|
|
1116
|
+
if (canonical === "AREAS" && expr.args.length === 1) {
|
|
1117
|
+
const raw = evaluate(expr.args[0], ctx, session);
|
|
1118
|
+
if (isError(raw)) {
|
|
1119
|
+
return raw;
|
|
1120
|
+
}
|
|
1121
|
+
if (raw.kind === RVKind.Reference) {
|
|
1122
|
+
return rvNumber(raw.areas.length);
|
|
1123
|
+
}
|
|
1124
|
+
// AREAS expects a reference-producing argument. Non-references
|
|
1125
|
+
// (literals, scalars, arrays) are rejected with #VALUE! in Excel,
|
|
1126
|
+
// not silently coerced to `1`. Previously we returned 1 which hid
|
|
1127
|
+
// caller bugs like `AREAS(42)` that should surface as errors.
|
|
1128
|
+
return ERRORS.VALUE;
|
|
1129
|
+
}
|
|
1010
1130
|
// Evaluate all arguments eagerly and dereference references
|
|
1011
1131
|
const args = expr.args.map(arg => dereferenceValue(evaluate(arg, ctx, session), ctx, session));
|
|
1012
|
-
// Look up function
|
|
1013
|
-
|
|
1132
|
+
// Look up function via the canonical (prefix-stripped) name computed
|
|
1133
|
+
// above. Previously we passed `expr.name`, forcing `lookupFunction`
|
|
1134
|
+
// to re-strip the prefix on every call; reusing `canonical` skips
|
|
1135
|
+
// that redundant check.
|
|
1136
|
+
//
|
|
1137
|
+
// User-registered functions take precedence over the built-in
|
|
1138
|
+
// registry — this lets callers shadow a built-in (e.g. replace
|
|
1139
|
+
// `IRR` with a domain-specific variant) or register entirely new
|
|
1140
|
+
// names (`MYFN`). The resulting descriptor still goes through the
|
|
1141
|
+
// same arity check.
|
|
1142
|
+
const userDesc = ctx.userFunctions?.get(canonical);
|
|
1143
|
+
if (userDesc) {
|
|
1144
|
+
// Validate arity first — same rule as built-ins.
|
|
1145
|
+
if (args.length < userDesc.minArity || args.length > userDesc.maxArity) {
|
|
1146
|
+
return ERRORS.VALUE;
|
|
1147
|
+
}
|
|
1148
|
+
// User-supplied code can throw; catch at the boundary so a buggy
|
|
1149
|
+
// custom function surfaces as `#VALUE!` rather than tearing down
|
|
1150
|
+
// the whole calculation pass. Any RuntimeValue return (including
|
|
1151
|
+
// error values the user constructed intentionally) passes through.
|
|
1152
|
+
try {
|
|
1153
|
+
return userDesc.invoke(args);
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
return ERRORS.VALUE;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const desc = lookupFunction(canonical);
|
|
1014
1160
|
if (desc) {
|
|
1015
1161
|
// Validate arity — produce #VALUE! for wrong argument count
|
|
1016
1162
|
if (args.length < desc.minArity || args.length > desc.maxArity) {
|
|
@@ -1033,60 +1179,10 @@ function evaluateCall(expr, ctx, session) {
|
|
|
1033
1179
|
}
|
|
1034
1180
|
return desc.invoke(args);
|
|
1035
1181
|
}
|
|
1036
|
-
case "ISFORMULA":
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
}
|
|
1182
|
+
case "ISFORMULA":
|
|
1183
|
+
return evaluateISFORMULA(expr.args, ctx, session);
|
|
1184
|
+
case "FORMULATEXT":
|
|
1185
|
+
return evaluateFORMULATEXT(expr.args, ctx, session);
|
|
1090
1186
|
default:
|
|
1091
1187
|
return desc.invoke(args);
|
|
1092
1188
|
}
|
|
@@ -1442,7 +1538,23 @@ function evaluateOFFSET(args, ctx, session) {
|
|
|
1442
1538
|
baseWidth = refExpr.right - refExpr.left + 1;
|
|
1443
1539
|
}
|
|
1444
1540
|
else {
|
|
1445
|
-
|
|
1541
|
+
// Evaluate — the base might be produced at runtime (INDIRECT, a
|
|
1542
|
+
// chained OFFSET, a defined name that bound to a reference, etc.).
|
|
1543
|
+
// Only accept single-area references; multi-area refs (3D / union)
|
|
1544
|
+
// are rejected by Excel too.
|
|
1545
|
+
const evaluated = evaluate(refExpr, ctx, session);
|
|
1546
|
+
if (isError(evaluated)) {
|
|
1547
|
+
return evaluated;
|
|
1548
|
+
}
|
|
1549
|
+
if (evaluated.kind !== RVKind.Reference || evaluated.areas.length !== 1) {
|
|
1550
|
+
return ERRORS.VALUE;
|
|
1551
|
+
}
|
|
1552
|
+
const area = evaluated.areas[0];
|
|
1553
|
+
baseRow = area.top;
|
|
1554
|
+
baseCol = area.left;
|
|
1555
|
+
baseSheet = area.sheet;
|
|
1556
|
+
baseHeight = area.bottom - area.top + 1;
|
|
1557
|
+
baseWidth = area.right - area.left + 1;
|
|
1446
1558
|
}
|
|
1447
1559
|
const rowsVal = topLeft(evalDeref(args[1], ctx, session));
|
|
1448
1560
|
const rowsNum = toNumberRV(rowsVal);
|
|
@@ -1563,19 +1675,50 @@ function evaluateMAP(args, ctx, session) {
|
|
|
1563
1675
|
if (args.length < 2) {
|
|
1564
1676
|
return ERRORS.VALUE;
|
|
1565
1677
|
}
|
|
1566
|
-
|
|
1678
|
+
// Excel's MAP accepts N source arrays + 1 lambda; the lambda must take
|
|
1679
|
+
// exactly N parameters. Previously this implementation only read the
|
|
1680
|
+
// first array and ignored args[1..N-1], so `MAP(A1:A3, B1:B3,
|
|
1681
|
+
// LAMBDA(a,b, a+b))` silently behaved as `MAP(A1:A3, LAMBDA(a, a))`.
|
|
1567
1682
|
const lambdaVal = evalDeref(args[args.length - 1], ctx, session);
|
|
1568
1683
|
if (!isLambda(lambdaVal)) {
|
|
1569
1684
|
return ERRORS.VALUE;
|
|
1570
1685
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1686
|
+
const arrayArgs = [];
|
|
1687
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
1688
|
+
arrayArgs.push(evalDeref(args[i], ctx, session));
|
|
1689
|
+
}
|
|
1690
|
+
if (lambdaVal.params.length !== arrayArgs.length) {
|
|
1691
|
+
return ERRORS.VALUE;
|
|
1692
|
+
}
|
|
1693
|
+
// Determine shape — all arrays must agree. Scalar args broadcast to
|
|
1694
|
+
// the majority shape. If every arg is a scalar, invoke the lambda
|
|
1695
|
+
// once with them all.
|
|
1696
|
+
let height = 1;
|
|
1697
|
+
let width = 1;
|
|
1698
|
+
for (const a of arrayArgs) {
|
|
1699
|
+
if (a.kind === RVKind.Array) {
|
|
1700
|
+
height = Math.max(height, a.height);
|
|
1701
|
+
width = Math.max(width, a.width);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
const allScalar = arrayArgs.every(a => a.kind !== RVKind.Array);
|
|
1705
|
+
if (allScalar) {
|
|
1706
|
+
return invokeLambda(lambdaVal, arrayArgs, ctx, session);
|
|
1707
|
+
}
|
|
1708
|
+
// Shape-mismatch rejection for arrays that neither broadcast nor
|
|
1709
|
+
// agree: each non-scalar array must match the target shape exactly
|
|
1710
|
+
// (Excel does not broadcast beyond scalar).
|
|
1711
|
+
for (const a of arrayArgs) {
|
|
1712
|
+
if (a.kind === RVKind.Array && (a.height !== height || a.width !== width)) {
|
|
1713
|
+
return ERRORS.VALUE;
|
|
1714
|
+
}
|
|
1573
1715
|
}
|
|
1574
1716
|
const rows = [];
|
|
1575
|
-
for (
|
|
1717
|
+
for (let r = 0; r < height; r++) {
|
|
1576
1718
|
const outRow = [];
|
|
1577
|
-
for (
|
|
1578
|
-
|
|
1719
|
+
for (let c = 0; c < width; c++) {
|
|
1720
|
+
const callArgs = arrayArgs.map(a => a.kind === RVKind.Array ? a.rows[r][c] : a);
|
|
1721
|
+
outRow.push(topLeft(invokeLambda(lambdaVal, callArgs, ctx, session)));
|
|
1579
1722
|
}
|
|
1580
1723
|
rows.push(outRow);
|
|
1581
1724
|
}
|
|
@@ -1591,10 +1734,20 @@ function evaluateREDUCE(args, ctx, session) {
|
|
|
1591
1734
|
if (!isLambda(lambdaVal)) {
|
|
1592
1735
|
return ERRORS.VALUE;
|
|
1593
1736
|
}
|
|
1737
|
+
// REDUCE's reducer takes exactly 2 parameters: the accumulator and
|
|
1738
|
+
// the current value. A mismatched arity silently bound `acc` but
|
|
1739
|
+
// left `value` undefined (or silently dropped extra params).
|
|
1740
|
+
if (lambdaVal.params.length !== 2) {
|
|
1741
|
+
return ERRORS.VALUE;
|
|
1742
|
+
}
|
|
1594
1743
|
if (arrVal.kind === RVKind.Array) {
|
|
1595
1744
|
for (const row of arrVal.rows) {
|
|
1596
1745
|
for (const cell of row) {
|
|
1597
|
-
acc
|
|
1746
|
+
// `acc` may legitimately be an array (the lambda returning one
|
|
1747
|
+
// is allowed — VSTACK inside the reducer, etc.). Only flatten
|
|
1748
|
+
// scalars when passing to the lambda; keeping the array shape
|
|
1749
|
+
// means subsequent iterations see the lambda's full output.
|
|
1750
|
+
acc = invokeLambda(lambdaVal, [acc, cell], ctx, session);
|
|
1598
1751
|
}
|
|
1599
1752
|
}
|
|
1600
1753
|
}
|
|
@@ -1602,7 +1755,7 @@ function evaluateREDUCE(args, ctx, session) {
|
|
|
1602
1755
|
// Scalar input: Excel treats the scalar as a 1×1 array and invokes the
|
|
1603
1756
|
// reducer exactly once. Previously we returned `init` unchanged, which
|
|
1604
1757
|
// silently broke `REDUCE(0, some_scalar, lambda)` callers.
|
|
1605
|
-
acc = invokeLambda(lambdaVal, [
|
|
1758
|
+
acc = invokeLambda(lambdaVal, [acc, topLeft(arrVal)], ctx, session);
|
|
1606
1759
|
}
|
|
1607
1760
|
return acc;
|
|
1608
1761
|
}
|
|
@@ -1616,12 +1769,20 @@ function evaluateSCAN(args, ctx, session) {
|
|
|
1616
1769
|
if (!isLambda(lambdaVal)) {
|
|
1617
1770
|
return ERRORS.VALUE;
|
|
1618
1771
|
}
|
|
1772
|
+
// SCAN's reducer takes (acc, value) — same arity rule as REDUCE.
|
|
1773
|
+
if (lambdaVal.params.length !== 2) {
|
|
1774
|
+
return ERRORS.VALUE;
|
|
1775
|
+
}
|
|
1619
1776
|
const rows = [];
|
|
1620
1777
|
if (arrVal.kind === RVKind.Array) {
|
|
1621
1778
|
for (const row of arrVal.rows) {
|
|
1622
1779
|
const outRow = [];
|
|
1623
1780
|
for (const cell of row) {
|
|
1624
|
-
acc
|
|
1781
|
+
// Pass `acc` through without topLeft so the lambda sees the full
|
|
1782
|
+
// previous result (matches the REDUCE fix). Each output cell
|
|
1783
|
+
// still captures the scalar top-left of `acc` so the result
|
|
1784
|
+
// grid stays rectangular.
|
|
1785
|
+
acc = invokeLambda(lambdaVal, [acc, cell], ctx, session);
|
|
1625
1786
|
outRow.push(topLeft(acc));
|
|
1626
1787
|
}
|
|
1627
1788
|
rows.push(outRow);
|
|
@@ -1631,7 +1792,7 @@ function evaluateSCAN(args, ctx, session) {
|
|
|
1631
1792
|
// Scalar input: emit a single-cell array containing the one accumulated
|
|
1632
1793
|
// value. Previously we returned #CALC! here, which was an artefact of the
|
|
1633
1794
|
// array-only implementation path.
|
|
1634
|
-
const result = invokeLambda(lambdaVal, [
|
|
1795
|
+
const result = invokeLambda(lambdaVal, [acc, topLeft(arrVal)], ctx, session);
|
|
1635
1796
|
return rvArray([[topLeft(result)]]);
|
|
1636
1797
|
}
|
|
1637
1798
|
function evaluateMAKEARRAY(args, ctx, session) {
|
|
@@ -1650,6 +1811,14 @@ function evaluateMAKEARRAY(args, ctx, session) {
|
|
|
1650
1811
|
if (!isLambda(lambdaVal)) {
|
|
1651
1812
|
return ERRORS.VALUE;
|
|
1652
1813
|
}
|
|
1814
|
+
// Excel's MAKEARRAY requires a 2-parameter lambda (row, col).
|
|
1815
|
+
// A 0 / 1 / 3+ param lambda is rejected at call time — previously we
|
|
1816
|
+
// invoked with 2 args regardless, which silently extended the lambda's
|
|
1817
|
+
// bindings past its declared parameter list (or left declared params
|
|
1818
|
+
// undefined).
|
|
1819
|
+
if (lambdaVal.params.length !== 2) {
|
|
1820
|
+
return ERRORS.VALUE;
|
|
1821
|
+
}
|
|
1653
1822
|
// Truncate toward zero and reject non-positive / overflow sizes.
|
|
1654
1823
|
// Without the cell-count cap the engine can silently allocate billions
|
|
1655
1824
|
// of scalars before blowing the heap; matching the broadcast limit
|
|
@@ -1681,9 +1850,21 @@ function evaluateBYROW(args, ctx, session) {
|
|
|
1681
1850
|
if (!isLambda(lambdaVal)) {
|
|
1682
1851
|
return ERRORS.VALUE;
|
|
1683
1852
|
}
|
|
1853
|
+
// BYROW requires a single-parameter lambda (the row). Mismatched arity
|
|
1854
|
+
// is rejected by Excel at call time — our previous impl silently left
|
|
1855
|
+
// extra params undefined.
|
|
1856
|
+
if (lambdaVal.params.length !== 1) {
|
|
1857
|
+
return ERRORS.VALUE;
|
|
1858
|
+
}
|
|
1684
1859
|
if (arrVal.kind !== RVKind.Array) {
|
|
1685
1860
|
return invokeLambda(lambdaVal, [rvArray([[topLeft(arrVal)]])], ctx, session);
|
|
1686
1861
|
}
|
|
1862
|
+
// Empty array (height 0) → Excel's BYROW reports `#CALC!` because
|
|
1863
|
+
// there is nothing to iterate over. Previously we returned an empty
|
|
1864
|
+
// array value which downstream arithmetic could not use.
|
|
1865
|
+
if (arrVal.height === 0 || arrVal.width === 0) {
|
|
1866
|
+
return ERRORS.CALC;
|
|
1867
|
+
}
|
|
1687
1868
|
const rows = [];
|
|
1688
1869
|
for (const row of arrVal.rows) {
|
|
1689
1870
|
const rowArr = rvArray([row.map(c => c)]);
|
|
@@ -1700,9 +1881,17 @@ function evaluateBYCOL(args, ctx, session) {
|
|
|
1700
1881
|
if (!isLambda(lambdaVal)) {
|
|
1701
1882
|
return ERRORS.VALUE;
|
|
1702
1883
|
}
|
|
1884
|
+
// BYCOL requires a single-parameter lambda (the column). See BYROW.
|
|
1885
|
+
if (lambdaVal.params.length !== 1) {
|
|
1886
|
+
return ERRORS.VALUE;
|
|
1887
|
+
}
|
|
1703
1888
|
if (arrVal.kind !== RVKind.Array) {
|
|
1704
1889
|
return invokeLambda(lambdaVal, [rvArray([[topLeft(arrVal)]])], ctx, session);
|
|
1705
1890
|
}
|
|
1891
|
+
// Empty array → `#CALC!` (see BYROW).
|
|
1892
|
+
if (arrVal.height === 0 || arrVal.width === 0) {
|
|
1893
|
+
return ERRORS.CALC;
|
|
1894
|
+
}
|
|
1706
1895
|
const numCols = arrVal.width;
|
|
1707
1896
|
const outRow = [];
|
|
1708
1897
|
for (let c = 0; c < numCols; c++) {
|
|
@@ -1765,6 +1954,179 @@ function tryEvaluateRefFunction(name, args, ctx) {
|
|
|
1765
1954
|
// ============================================================================
|
|
1766
1955
|
// Reference-aware: ISREF
|
|
1767
1956
|
// ============================================================================
|
|
1957
|
+
/**
|
|
1958
|
+
* Resolve the top-left cell targeted by an ISFORMULA / FORMULATEXT argument.
|
|
1959
|
+
*
|
|
1960
|
+
* Unlike `resolveCellRefArg` (used by CELL), these two functions must return
|
|
1961
|
+
* `#N/A` for non-reference arguments (arithmetic, literals, ...) rather than
|
|
1962
|
+
* `#VALUE!`. We support:
|
|
1963
|
+
* - purely syntactic reference nodes (CellRef / AreaRef / ColRangeRef /
|
|
1964
|
+
* RowRangeRef / Ref3D) — cheapest path, no evaluation needed
|
|
1965
|
+
* - runtime-produced references (INDIRECT, OFFSET) — we evaluate without
|
|
1966
|
+
* dereferencing and inspect the resulting ReferenceValue
|
|
1967
|
+
*
|
|
1968
|
+
* Anything else — including errors from INDIRECT("xx") — collapses to
|
|
1969
|
+
* `#N/A`, matching Excel's tolerant behaviour for these two functions.
|
|
1970
|
+
*/
|
|
1971
|
+
function resolveFormulaRefArg(arg, ctx, session) {
|
|
1972
|
+
if (!arg) {
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
// Syntactic reference forms — extract top-left directly, no evaluation.
|
|
1976
|
+
if (arg.kind === BoundExprKind.CellRef) {
|
|
1977
|
+
return { sheet: arg.sheet, row: arg.row, col: arg.col };
|
|
1978
|
+
}
|
|
1979
|
+
if (arg.kind === BoundExprKind.AreaRef) {
|
|
1980
|
+
return { sheet: arg.sheet, row: arg.top, col: arg.left };
|
|
1981
|
+
}
|
|
1982
|
+
if (arg.kind === BoundExprKind.ColRangeRef) {
|
|
1983
|
+
const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
|
|
1984
|
+
const top = ws?.dimensions?.top ?? 1;
|
|
1985
|
+
return { sheet: arg.sheet, row: top, col: arg.leftCol };
|
|
1986
|
+
}
|
|
1987
|
+
if (arg.kind === BoundExprKind.RowRangeRef) {
|
|
1988
|
+
const ws = ctx.snapshot.worksheetsByName.get(arg.sheet.toLowerCase());
|
|
1989
|
+
const left = ws?.dimensions?.left ?? 1;
|
|
1990
|
+
return { sheet: arg.sheet, row: arg.topRow, col: left };
|
|
1991
|
+
}
|
|
1992
|
+
if (arg.kind === BoundExprKind.Ref3D) {
|
|
1993
|
+
const first = arg.sheets[0];
|
|
1994
|
+
if (first === undefined) {
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
if (arg.inner.kind === BoundExprKind.CellRef) {
|
|
1998
|
+
return { sheet: first, row: arg.inner.row, col: arg.inner.col };
|
|
1999
|
+
}
|
|
2000
|
+
return { sheet: first, row: arg.inner.top, col: arg.inner.left };
|
|
2001
|
+
}
|
|
2002
|
+
// Evaluate without dereferencing — INDIRECT/OFFSET may yield a
|
|
2003
|
+
// ReferenceValue. Errors or non-references collapse to null (→ #N/A).
|
|
2004
|
+
const raw = evaluate(arg, ctx, session);
|
|
2005
|
+
if (raw.kind === RVKind.Reference && raw.areas.length > 0) {
|
|
2006
|
+
const area = raw.areas[0];
|
|
2007
|
+
return { sheet: area.sheet, row: area.top, col: area.left };
|
|
2008
|
+
}
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
function evaluateISFORMULA(args, ctx, session) {
|
|
2012
|
+
if (args.length !== 1) {
|
|
2013
|
+
return ERRORS.NA;
|
|
2014
|
+
}
|
|
2015
|
+
const target = resolveFormulaRefArg(args[0], ctx, session);
|
|
2016
|
+
if (!target) {
|
|
2017
|
+
return ERRORS.NA;
|
|
2018
|
+
}
|
|
2019
|
+
const ws = ctx.snapshot.worksheetsByName.get(target.sheet.toLowerCase());
|
|
2020
|
+
if (!ws) {
|
|
2021
|
+
return ERRORS.REF;
|
|
2022
|
+
}
|
|
2023
|
+
const cell = ws.cells.get(snapshotCellKey(target.row, target.col));
|
|
2024
|
+
return rvBoolean(cell !== undefined && cell.formulaKind !== "none");
|
|
2025
|
+
}
|
|
2026
|
+
function evaluateFORMULATEXT(args, ctx, session) {
|
|
2027
|
+
if (args.length !== 1) {
|
|
2028
|
+
return ERRORS.NA;
|
|
2029
|
+
}
|
|
2030
|
+
const target = resolveFormulaRefArg(args[0], ctx, session);
|
|
2031
|
+
if (!target) {
|
|
2032
|
+
return ERRORS.NA;
|
|
2033
|
+
}
|
|
2034
|
+
const ws = ctx.snapshot.worksheetsByName.get(target.sheet.toLowerCase());
|
|
2035
|
+
if (!ws) {
|
|
2036
|
+
return ERRORS.REF;
|
|
2037
|
+
}
|
|
2038
|
+
const cell = ws.cells.get(snapshotCellKey(target.row, target.col));
|
|
2039
|
+
if (cell && cell.formulaKind !== "none" && cell.formula !== undefined) {
|
|
2040
|
+
return rvString(`=${cell.formula}`);
|
|
2041
|
+
}
|
|
2042
|
+
return ERRORS.NA;
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* INDEX reference-aware path.
|
|
2046
|
+
*
|
|
2047
|
+
* Standard INDEX takes `(array, row, col)` and returns a value or
|
|
2048
|
+
* sub-range. When the first argument is a multi-area UnionRef, INDEX
|
|
2049
|
+
* also accepts a fourth `area_num` argument to select which area to
|
|
2050
|
+
* index into — e.g. `INDEX((A1:B2, D4:E5), 1, 1, 2) = D4`.
|
|
2051
|
+
*
|
|
2052
|
+
* We intercept INDEX here (before the normal eager-dereference path)
|
|
2053
|
+
* because dereferencing a multi-area ReferenceValue flattens its
|
|
2054
|
+
* areas into a single stacked array, losing the per-area boundary
|
|
2055
|
+
* needed for `area_num`. For the common single-area case we return
|
|
2056
|
+
* `undefined` so the eager path runs and delegates to `fnINDEX`.
|
|
2057
|
+
*/
|
|
2058
|
+
function tryEvaluateINDEX(args, ctx, session) {
|
|
2059
|
+
if (args.length < 2 || args.length > 4) {
|
|
2060
|
+
return undefined; // Fall through — eager fnINDEX reports arity errors.
|
|
2061
|
+
}
|
|
2062
|
+
// Fast path: 2- and 3-arg INDEX has no area_num, so we can skip the
|
|
2063
|
+
// union-aware logic entirely for the common single-array case. Only
|
|
2064
|
+
// INDEX with an explicit area_num (4 args) OR a multi-area first
|
|
2065
|
+
// operand needs the reference-aware route.
|
|
2066
|
+
const first = evaluate(args[0], ctx, session);
|
|
2067
|
+
if (isError(first)) {
|
|
2068
|
+
// Errors from the source expression propagate regardless of arity.
|
|
2069
|
+
return first;
|
|
2070
|
+
}
|
|
2071
|
+
const isMultiArea = first.kind === RVKind.Reference && first.areas.length > 1;
|
|
2072
|
+
if (!isMultiArea && args.length < 4) {
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}
|
|
2075
|
+
// Single-area reference with an explicit 4th arg still needs
|
|
2076
|
+
// area_num validation — Excel rejects `INDEX(A1:B2, 1, 1, 2)` as
|
|
2077
|
+
// #REF! since the source only has one area.
|
|
2078
|
+
if (first.kind !== RVKind.Reference || first.areas.length === 0) {
|
|
2079
|
+
// Non-reference first arg (array literal, number, etc.) — only
|
|
2080
|
+
// valid when args.length < 4. Let the eager path handle it.
|
|
2081
|
+
if (args.length < 4) {
|
|
2082
|
+
return undefined;
|
|
2083
|
+
}
|
|
2084
|
+
// With an explicit area_num on a non-reference, Excel returns #REF!.
|
|
2085
|
+
return ERRORS.REF;
|
|
2086
|
+
}
|
|
2087
|
+
const areas = first.areas;
|
|
2088
|
+
// Resolve `area_num` (1-based). Omitted / Blank → area 1. Truncate
|
|
2089
|
+
// toward zero and bounds-check against the union's cardinality.
|
|
2090
|
+
let areaNum = 1;
|
|
2091
|
+
if (args.length === 4) {
|
|
2092
|
+
const rawArea = dereferenceValue(evaluate(args[3], ctx, session), ctx, session);
|
|
2093
|
+
if (isError(rawArea)) {
|
|
2094
|
+
return rawArea;
|
|
2095
|
+
}
|
|
2096
|
+
const s = topLeft(rawArea);
|
|
2097
|
+
// Treat a blank / missing 4th argument as the default (area 1) —
|
|
2098
|
+
// matches Excel's tolerance for `INDEX(ref, r, c, )` and avoids the
|
|
2099
|
+
// surprise where a trailing comma would silently produce #REF!
|
|
2100
|
+
// (blank → toNumberRV = 0 → out-of-range).
|
|
2101
|
+
if (s.kind !== RVKind.Blank) {
|
|
2102
|
+
const aRV = toNumberRV(s);
|
|
2103
|
+
if (isError(aRV)) {
|
|
2104
|
+
return aRV;
|
|
2105
|
+
}
|
|
2106
|
+
areaNum = Math.trunc(aRV.value);
|
|
2107
|
+
if (areaNum < 1 || areaNum > areas.length) {
|
|
2108
|
+
return ERRORS.REF;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
const area = areas[areaNum - 1];
|
|
2113
|
+
// Build a single-area ArrayValue for the selected region, then
|
|
2114
|
+
// delegate to `fnINDEX` using the row/col args. This keeps the
|
|
2115
|
+
// actual indexing logic in one place.
|
|
2116
|
+
const selectedArr = buildRangeArray(ctx, session, area.sheet, area.top, area.left, area.bottom, area.right);
|
|
2117
|
+
const indexArgs = [selectedArr];
|
|
2118
|
+
for (let i = 1; i < Math.min(args.length, 3); i++) {
|
|
2119
|
+
indexArgs.push(dereferenceValue(evaluate(args[i], ctx, session), ctx, session));
|
|
2120
|
+
}
|
|
2121
|
+
// Delegate to the registered INDEX implementation — keeps the actual
|
|
2122
|
+
// indexing logic (fractional truncation, single-col collapse, row=0
|
|
2123
|
+
// / col=0 semantics) in one place rather than reimplementing here.
|
|
2124
|
+
const indexFn = lookupFunction("INDEX");
|
|
2125
|
+
if (!indexFn) {
|
|
2126
|
+
return ERRORS.VALUE;
|
|
2127
|
+
}
|
|
2128
|
+
return indexFn.invoke(indexArgs);
|
|
2129
|
+
}
|
|
1768
2130
|
/**
|
|
1769
2131
|
* ISREF(value) → TRUE if `value` is a reference; FALSE otherwise.
|
|
1770
2132
|
*
|
|
@@ -1908,7 +2270,23 @@ function evaluateCELL(args, ctx, session) {
|
|
|
1908
2270
|
}
|
|
1909
2271
|
switch (info) {
|
|
1910
2272
|
case "address": {
|
|
1911
|
-
|
|
2273
|
+
// Excel qualifies the address with the sheet name when the target
|
|
2274
|
+
// sheet differs from the formula-cell's own sheet (e.g.
|
|
2275
|
+
// `CELL("address", Sheet2!A1)` → `"Sheet2!$A$1"`). Same-sheet
|
|
2276
|
+
// refs stay unqualified. Without the qualifier, callers that
|
|
2277
|
+
// parse the result (INDIRECT(CELL("address", ref))) lose the
|
|
2278
|
+
// sheet context and misread remote cells.
|
|
2279
|
+
const addr = `$${colNumberToLetter(target.col)}$${target.row}`;
|
|
2280
|
+
const sameSheet = ctx.currentSheet.toLowerCase() === target.sheet.toLowerCase();
|
|
2281
|
+
if (sameSheet) {
|
|
2282
|
+
return rvString(addr);
|
|
2283
|
+
}
|
|
2284
|
+
// Quote sheet names that need it (spaces, special chars, starts
|
|
2285
|
+
// with digit). The same rule the tokenizer uses when parsing
|
|
2286
|
+
// quoted sheet refs on the way in.
|
|
2287
|
+
const needsQuote = !/^[A-Za-z_][A-Za-z0-9_]*$/.test(target.sheet);
|
|
2288
|
+
const sheetPrefix = needsQuote ? `'${target.sheet.replace(/'/g, "''")}'` : target.sheet;
|
|
2289
|
+
return rvString(`${sheetPrefix}!${addr}`);
|
|
1912
2290
|
}
|
|
1913
2291
|
case "row":
|
|
1914
2292
|
return rvNumber(target.row);
|
|
@@ -1957,6 +2335,16 @@ function evaluateNameExpr(expr, ctx, session) {
|
|
|
1957
2335
|
const rangeStr = dn.ranges[0];
|
|
1958
2336
|
const parsed = parseDefinedNameRange(rangeStr);
|
|
1959
2337
|
if (parsed) {
|
|
2338
|
+
// Validate against Excel's sheet coordinate limits before we pass
|
|
2339
|
+
// the values to `getCellValue` / `buildRangeArray`. An invalid
|
|
2340
|
+
// defined-name string (e.g. a range exceeding column XFD) should
|
|
2341
|
+
// surface as #REF! rather than silently reading BLANK cells.
|
|
2342
|
+
if (parsed.startRow < 1 ||
|
|
2343
|
+
parsed.endRow > 1048576 ||
|
|
2344
|
+
parsed.startCol < 1 ||
|
|
2345
|
+
parsed.endCol > 16384) {
|
|
2346
|
+
return ERRORS.REF;
|
|
2347
|
+
}
|
|
1960
2348
|
if (parsed.startRow === parsed.endRow && parsed.startCol === parsed.endCol) {
|
|
1961
2349
|
return getCellValue(parsed.sheet, parsed.startRow, parsed.startCol, ctx, session);
|
|
1962
2350
|
}
|
|
@@ -2017,6 +2405,38 @@ function evaluateLambdaExpr(expr, ctx) {
|
|
|
2017
2405
|
return rvLambda([...expr.params], expr.body, ctx.localBindings ? new Map(ctx.localBindings) : undefined);
|
|
2018
2406
|
}
|
|
2019
2407
|
// ============================================================================
|
|
2408
|
+
// Union Reference `(A1:B2, D4:E5)` — yields a multi-area ReferenceValue
|
|
2409
|
+
// ============================================================================
|
|
2410
|
+
/**
|
|
2411
|
+
* Evaluate a reference union syntactically formed by `(area1, area2, …)`.
|
|
2412
|
+
*
|
|
2413
|
+
* Each member must resolve to a reference-producing value. The resulting
|
|
2414
|
+
* `ReferenceValue` carries every area in order so that `INDEX(union, r,
|
|
2415
|
+
* c, area_num)` can pick the right one. Any non-reference member
|
|
2416
|
+
* (scalar / array literal / error) short-circuits to `#VALUE!`.
|
|
2417
|
+
*/
|
|
2418
|
+
function evaluateUnionRef(expr, ctx, session) {
|
|
2419
|
+
const areas = [];
|
|
2420
|
+
for (const member of expr.areas) {
|
|
2421
|
+
const val = evaluate(member, ctx, session);
|
|
2422
|
+
if (isError(val)) {
|
|
2423
|
+
return val;
|
|
2424
|
+
}
|
|
2425
|
+
if (val.kind !== RVKind.Reference || val.areas.length === 0) {
|
|
2426
|
+
// Excel rejects non-reference members of a union outright —
|
|
2427
|
+
// `(A1, "text")` is `#VALUE!`, not a silent coerce to 1-cell.
|
|
2428
|
+
return ERRORS.VALUE;
|
|
2429
|
+
}
|
|
2430
|
+
for (const a of val.areas) {
|
|
2431
|
+
areas.push(a);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
if (areas.length === 0) {
|
|
2435
|
+
return ERRORS.VALUE;
|
|
2436
|
+
}
|
|
2437
|
+
return { kind: RVKind.Reference, areas };
|
|
2438
|
+
}
|
|
2439
|
+
// ============================================================================
|
|
2020
2440
|
// Structured Reference (runtime resolution)
|
|
2021
2441
|
// ============================================================================
|
|
2022
2442
|
function evaluateStructuredRef(expr, ctx, session) {
|