@borgar/fx 4.7.1 → 4.8.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.
@@ -0,0 +1,96 @@
1
+ /* eslint-disable jsdoc/require-property-description */
2
+
3
+ /**
4
+ * @typedef {number[]} SourceLocation
5
+ */
6
+
7
+ /**
8
+ * @typedef {Node} Identifier
9
+ * @property {"Identifier"} type
10
+ * @property {SourceLocation} [loc]
11
+ * @property {string} name
12
+ */
13
+
14
+ /**
15
+ * @typedef {Node} ReferenceIdentifier
16
+ * @property {"ReferenceIdentifier"} type
17
+ * @property {SourceLocation} [loc]
18
+ * @property {string} value
19
+ * @property {"name" | "range" | "beam" | "table"} kind
20
+ */
21
+
22
+ /**
23
+ * @typedef {Node} Literal
24
+ * @property {"Literal"} type
25
+ * @property {SourceLocation} [loc]
26
+ * @property {string} raw
27
+ * @property {string | number | boolean} value
28
+ */
29
+
30
+ /**
31
+ * @typedef {Node} ErrorLiteral
32
+ * @property {"ErrorLiteral"} type
33
+ * @property {SourceLocation} [loc]
34
+ * @property {string} raw
35
+ * @property {string} value
36
+ */
37
+
38
+ /**
39
+ * @typedef {Node} UnaryExpression
40
+ * @property {"UnaryExpression"} type
41
+ * @property {SourceLocation} [loc]
42
+ * @property {"+" | "-" | "%" | "#" | "@"} operator
43
+ * @property {AstExpression[]} arguments
44
+ */
45
+
46
+ /**
47
+ * @typedef {Node} BinaryExpression
48
+ * @property {"BinaryExpression"} type
49
+ * @property {SourceLocation} [loc]
50
+ * @property {"=" | "<" | ">" | "<=" | ">=" | "<>" | "-" | "+" | "*" | "/" | "^" | ":" | " " | "," | "&"} operator
51
+ * @property {AstExpression[]} arguments
52
+ */
53
+
54
+ /**
55
+ * @typedef {Node} CallExpression
56
+ * @property {"CallExpression"} type
57
+ * @property {SourceLocation} [loc]
58
+ * @property {Identifier} callee
59
+ * @property {AstExpression[]} arguments
60
+ */
61
+
62
+ // FIXME: the awkward naming is because tooling fails, fix tooling :)
63
+ /**
64
+ * @typedef {Node} MatrixExpression
65
+ * @property {"ArrayExpression"} type
66
+ * @property {SourceLocation} [loc]
67
+ * @property {Array<Array<ReferenceIdentifier | Literal | ErrorLiteral | CallExpression>>} arguments
68
+ */
69
+
70
+ /**
71
+ * @typedef {Node} LambdaExpression
72
+ * @property {"LambdaExpression"} type
73
+ * @property {SourceLocation} [loc]
74
+ * @property {Identifier[]} params
75
+ * @property {null | AstExpression} body
76
+ */
77
+
78
+ /**
79
+ * @typedef {Node} LetExpression
80
+ * @property {"LetExpression"} type
81
+ * @property {SourceLocation} [loc]
82
+ * @property {LetDeclarator[]} declarations
83
+ * @property {null | AstExpression} body
84
+ */
85
+
86
+ /**
87
+ * @typedef {Node} LetDeclarator
88
+ * @property {"LetDeclarator"} type
89
+ * @property {SourceLocation} [loc]
90
+ * @property {Identifier} id
91
+ * @property {null | AstExpression} init
92
+ */
93
+
94
+ /**
95
+ * @typedef {ReferenceIdentifier | Literal | ErrorLiteral | UnaryExpression | BinaryExpression | CallExpression | MatrixExpression | LambdaExpression | LetExpression} AstExpression
96
+ */
package/lib/constants.js CHANGED
@@ -22,8 +22,11 @@ export const REFERENCE = 'ReferenceIdentifier';
22
22
  export const LITERAL = 'Literal';
23
23
  export const ERROR_LITERAL = 'ErrorLiteral';
24
24
  export const CALL = 'CallExpression';
25
+ export const LAMBDA = 'LambdaExpression';
26
+ export const LET = 'LetExpression';
25
27
  export const ARRAY = 'ArrayExpression';
26
28
  export const IDENTIFIER = 'Identifier';
29
+ export const LET_DECL = 'LetDeclarator';
27
30
 
28
31
  export const MAX_COLS = (2 ** 14) - 1; // 16383
29
32
  export const MAX_ROWS = (2 ** 20) - 1; // 1048575
package/lib/lexer.js CHANGED
@@ -38,11 +38,43 @@ const causesBinaryMinus = token => {
38
38
  );
39
39
  };
40
40
 
41
+ function fixRCNames (tokens) {
42
+ let withinCall = 0;
43
+ let parenDepth = 0;
44
+ let lastToken;
45
+ for (const token of tokens) {
46
+ if (token.type === OPERATOR) {
47
+ if (token.value === '(') {
48
+ parenDepth++;
49
+ if (lastToken.type === FUNCTION) {
50
+ const v = lastToken.value.toLowerCase();
51
+ if (v === 'lambda' || v === 'let') {
52
+ withinCall = parenDepth;
53
+ }
54
+ }
55
+ }
56
+ else if (token.value === ')') {
57
+ parenDepth--;
58
+ if (parenDepth < withinCall) {
59
+ withinCall = 0;
60
+ }
61
+ }
62
+ }
63
+ else if (withinCall && token.type === UNKNOWN && /^[rc]$/.test(token.value)) {
64
+ token.type = REF_NAMED;
65
+ }
66
+ lastToken = token;
67
+ }
68
+ return tokens;
69
+ }
70
+
41
71
  export function getTokens (fx, tokenHandlers, options = {}) {
42
72
  const opts = Object.assign({}, defaultOptions, options);
43
73
  const { withLocation, mergeRefs, negativeNumbers } = opts;
44
74
  const tokens = [];
45
75
  let pos = 0;
76
+ let letOrLambda = 0;
77
+ let unknownRC = 0;
46
78
 
47
79
  let tail0 = null; // last non-whitespace token
48
80
  let tail1 = null; // penultimate non-whitespace token
@@ -110,6 +142,19 @@ export function getTokens (fx, tokenHandlers, options = {}) {
110
142
  ...(withLocation ? { loc: [ startPos, pos ] } : {})
111
143
  };
112
144
 
145
+ // make a note if we found a let/lambda call
146
+ if (lastToken && lastToken.type === FUNCTION && tokenValue === '(') {
147
+ const lastLC = lastToken.value.toLowerCase();
148
+ if (lastLC === 'lambda' || lastLC === 'let') {
149
+ letOrLambda++;
150
+ }
151
+ }
152
+ // make a note if we found a R or C unknown
153
+ if (tokenType === UNKNOWN) {
154
+ const valLC = tokenValue.toLowerCase();
155
+ unknownRC += (valLC === 'r' || valLC === 'c') ? 1 : 0;
156
+ }
157
+
113
158
  // check for termination
114
159
  if (tokenType === STRING) {
115
160
  const l = tokenValue.length;
@@ -157,6 +202,12 @@ export function getTokens (fx, tokenHandlers, options = {}) {
157
202
  pushToken(token);
158
203
  }
159
204
 
205
+ // if we encountered both a LAMBDA/LET call, and unknown 'r' or 'c' tokens
206
+ // we'll turn the unknown tokens into names within the call.
207
+ if (unknownRC && letOrLambda) {
208
+ fixRCNames(tokens);
209
+ }
210
+
160
211
  if (mergeRefs) {
161
212
  return mergeRefTokens(tokens);
162
213
  }
package/lib/lexer.spec.js CHANGED
@@ -1462,6 +1462,18 @@ test('unknowns, named ranges and functions', t => {
1462
1462
  { type: FX_PREFIX, value: '=' },
1463
1463
  { type: REF_NAMED, value: '\\foo' }
1464
1464
  ]);
1465
+ t.isTokens('=\\fo', [
1466
+ { type: FX_PREFIX, value: '=' },
1467
+ { type: REF_NAMED, value: '\\fo' }
1468
+ ]);
1469
+ t.isTokens('=\\f', [
1470
+ { type: FX_PREFIX, value: '=' },
1471
+ { type: UNKNOWN, value: '\\f' }
1472
+ ]);
1473
+ t.isTokens('=\\', [
1474
+ { type: FX_PREFIX, value: '=' },
1475
+ { type: UNKNOWN, value: '\\' }
1476
+ ]);
1465
1477
  t.isTokens('=æði', [
1466
1478
  { type: FX_PREFIX, value: '=' },
1467
1479
  { type: REF_NAMED, value: 'æði' }
@@ -1769,3 +1781,54 @@ test('tokenize external refs syntax from XLSX files', t => {
1769
1781
 
1770
1782
  t.end();
1771
1783
  });
1784
+
1785
+ test('tokenize r and c as names within LET and LAMBDA calls', t => {
1786
+ t.isTokens('=c*(LAMBDA(r,c,r*c)+r)+r', [
1787
+ { type: FX_PREFIX, value: '=' },
1788
+ { type: UNKNOWN, value: 'c' },
1789
+ { type: OPERATOR, value: '*' },
1790
+ { type: OPERATOR, value: '(' },
1791
+ { type: FUNCTION, value: 'LAMBDA' },
1792
+ { type: OPERATOR, value: '(' },
1793
+ { type: REF_NAMED, value: 'r' },
1794
+ { type: OPERATOR, value: ',' },
1795
+ { type: REF_NAMED, value: 'c' },
1796
+ { type: OPERATOR, value: ',' },
1797
+ { type: REF_NAMED, value: 'r' },
1798
+ { type: OPERATOR, value: '*' },
1799
+ { type: REF_NAMED, value: 'c' },
1800
+ { type: OPERATOR, value: ')' },
1801
+ { type: OPERATOR, value: '+' },
1802
+ { type: UNKNOWN, value: 'r' },
1803
+ { type: OPERATOR, value: ')' },
1804
+ { type: OPERATOR, value: '+' },
1805
+ { type: UNKNOWN, value: 'r' }
1806
+ ]);
1807
+ t.isTokens('=c*(LET(r,A1,c,B2,r*c)+r)+r', [
1808
+ { type: FX_PREFIX, value: '=' },
1809
+ { type: UNKNOWN, value: 'c' },
1810
+ { type: OPERATOR, value: '*' },
1811
+ { type: OPERATOR, value: '(' },
1812
+ { type: FUNCTION, value: 'LET' },
1813
+ { type: OPERATOR, value: '(' },
1814
+ { type: REF_NAMED, value: 'r' },
1815
+ { type: OPERATOR, value: ',' },
1816
+ { type: REF_RANGE, value: 'A1' },
1817
+ { type: OPERATOR, value: ',' },
1818
+ { type: REF_NAMED, value: 'c' },
1819
+ { type: OPERATOR, value: ',' },
1820
+ { type: REF_RANGE, value: 'B2' },
1821
+ { type: OPERATOR, value: ',' },
1822
+ { type: REF_NAMED, value: 'r' },
1823
+ { type: OPERATOR, value: '*' },
1824
+ { type: REF_NAMED, value: 'c' },
1825
+ { type: OPERATOR, value: ')' },
1826
+ { type: OPERATOR, value: '+' },
1827
+ { type: UNKNOWN, value: 'r' },
1828
+ { type: OPERATOR, value: ')' },
1829
+ { type: OPERATOR, value: '+' },
1830
+ { type: UNKNOWN, value: 'r' }
1831
+ ]);
1832
+
1833
+ t.end();
1834
+ });
package/lib/lexerParts.js CHANGED
@@ -67,6 +67,11 @@ function lexNamed (str) {
67
67
  const m = re_NAMED.exec(str);
68
68
  if (m) {
69
69
  const lc = m[0].toLowerCase();
70
+ // names starting with \ must be at least 3 char long
71
+ if (lc[0] === '\\' && m[0].length < 3) {
72
+ return null;
73
+ }
74
+ // single characters R and C are forbidden as names
70
75
  if (lc === 'r' || lc === 'c') {
71
76
  return null;
72
77
  }
package/lib/parser.js CHANGED
@@ -6,14 +6,17 @@
6
6
  * Beutiful Code (http://crockford.com/javascript/tdop/tdop.html).
7
7
  *
8
8
  * The parser handles most basic things Excel/Sheets do except:
9
- *
10
- * - LAMBDA expressions: =LAMBDA(x, x*x)(2)
11
- * https://support.microsoft.com/en-us/office/lambda-function-bd212d27-1cd1-4321-a34a-ccbf254b8b67
12
- * - LET expressions: LET(x, 5, SUM(x, 1))
13
- * https://support.microsoft.com/en-us/office/let-function-34842dd8-b92b-4d3f-b325-b8b8f9908999
14
- * - Sheet1:Sheet2!A1 references cross contexts (3D references)
9
+ * `Sheet1:Sheet2!A1` references cross contexts (3D references)
15
10
  */
16
- import { isReference, isLiteral, isFunction, isWhitespace, isFxPrefix, isOperator, isError } from './isType.js';
11
+ import {
12
+ isReference,
13
+ isLiteral,
14
+ isFunction,
15
+ isWhitespace,
16
+ isFxPrefix,
17
+ isOperator,
18
+ isError
19
+ } from './isType.js';
17
20
  import {
18
21
  UNARY,
19
22
  BINARY,
@@ -21,12 +24,18 @@ import {
21
24
  LITERAL,
22
25
  ERROR_LITERAL,
23
26
  CALL,
27
+ LAMBDA,
24
28
  ARRAY,
25
29
  IDENTIFIER,
26
30
  NUMBER,
27
31
  BOOLEAN,
28
32
  ERROR,
29
- STRING
33
+ STRING,
34
+ LET,
35
+ LET_DECL,
36
+ REF_NAMED,
37
+ REF_STRUCT,
38
+ REF_BEAM
30
39
  } from './constants.js';
31
40
 
32
41
  import { tokenize } from './lexer.js';
@@ -53,12 +62,24 @@ const refFunctions = [
53
62
  'XLOOKUP'
54
63
  ];
55
64
 
56
- const isReferenceToken = token => {
65
+ const isReferenceFunctionName = fnName => {
66
+ return refFunctions.includes(fnName.toUpperCase());
67
+ };
68
+
69
+ const isReferenceToken = (token, allowOperators = false) => {
57
70
  const value = (token && token.value) + '';
58
- if (isReference(token)) { return true; }
59
- if (isOperator(token) && (value === ':' || value === ',' || !value.trim())) { return true; } // join, union, intersection
60
- if (isFunction(token) && refFunctions.includes(value.toUpperCase())) { return true; } // intersection
61
- if (isError(token) && value === '#REF!') { return true; }
71
+ if (isReference(token)) {
72
+ return true;
73
+ }
74
+ if (allowOperators && isOperator(token) && (value === ':' || value === ',' || !value.trim())) {
75
+ return true; // join, union, intersection
76
+ }
77
+ if (isFunction(token) && isReferenceFunctionName(value)) {
78
+ return true; // function that yields reference
79
+ }
80
+ if (isError(token) && value === '#REF!') {
81
+ return true;
82
+ }
62
83
  return false;
63
84
  };
64
85
 
@@ -71,7 +92,8 @@ const isReferenceNode = node => {
71
92
  node.operator === ' ' ||
72
93
  node.operator === ',')
73
94
  ) ||
74
- (node.type === CALL && refFunctions.includes(node.callee.name.toUpperCase()))
95
+ isReference(node) ||
96
+ (node.type === CALL && isReferenceFunctionName(node.callee.name))
75
97
  );
76
98
  };
77
99
 
@@ -82,15 +104,17 @@ let tokenIndex;
82
104
  let permitArrayRanges = false;
83
105
  let permitArrayCalls = false;
84
106
 
85
- function halt (message) {
107
+ function halt (message, atIndex = null) {
86
108
  const err = new Error(message);
87
109
  err.source = tokens.map(d => d.value).join('');
88
- err.sourceOffset = tokens.slice(0, tokenIndex).reduce((a, d) => a + d.value, '').length;
110
+ err.sourceOffset = tokens
111
+ .slice(0, atIndex ?? tokenIndex)
112
+ .reduce((a, d) => a + d.value.length, 0);
89
113
  throw err;
90
114
  }
91
115
 
92
116
  // A1 A1 | A1 (A1) | A1 ((A1)) | A1 ( (A1) ) | ...
93
- function refIsUpcoming () {
117
+ function refIsUpcoming (allowOperators = false) {
94
118
  let i = tokenIndex;
95
119
  let next;
96
120
  do {
@@ -102,7 +126,7 @@ function refIsUpcoming () {
102
126
  (isOperator(next) && next.value === '(')
103
127
  )
104
128
  );
105
- return isReferenceToken(next);
129
+ return isReferenceToken(next, allowOperators);
106
130
  }
107
131
 
108
132
  function advance (expectNext = null, leftNode = null) {
@@ -111,9 +135,11 @@ function advance (expectNext = null, leftNode = null) {
111
135
  }
112
136
  // look ahead to see if we have ( ( " ", "(" )+ REF )
113
137
  if (isWhitespace(tokens[tokenIndex])) {
114
- // potential intersection operation
115
- const possibleWSOp = isReferenceNode(leftNode) && refIsUpcoming();
116
- if (!possibleWSOp) {
138
+ // potential intersection operation (so don't allow operators as upcoming)
139
+ const haveRef = isReferenceNode(leftNode);
140
+ const possibleWSOp = haveRef && refIsUpcoming(false);
141
+ const nextIsCall = haveRef && tokens[tokenIndex + 1].value === '(';
142
+ if (!possibleWSOp && !nextIsCall) {
117
143
  // ignore whitespace
118
144
  while (isWhitespace(tokens[tokenIndex])) {
119
145
  tokenIndex++;
@@ -134,7 +160,6 @@ function advance (expectNext = null, leftNode = null) {
134
160
  }
135
161
 
136
162
  let node;
137
- let type = token.type;
138
163
  if (isOperator(token)) {
139
164
  node = symbolTable[token.value];
140
165
  if (!node) {
@@ -149,7 +174,6 @@ function advance (expectNext = null, leftNode = null) {
149
174
  }
150
175
  else if (isReference(token)) {
151
176
  node = symbolTable[REFERENCE];
152
- type = REFERENCE;
153
177
  }
154
178
  else if (isFunction(token)) {
155
179
  node = symbolTable[FUNCTION];
@@ -159,7 +183,7 @@ function advance (expectNext = null, leftNode = null) {
159
183
  }
160
184
 
161
185
  currentNode = Object.create(node);
162
- currentNode.type = type;
186
+ currentNode.type = token.type;
163
187
  currentNode.value = token.value;
164
188
  if (token.loc) {
165
189
  currentNode.loc = [ ...token.loc ];
@@ -289,7 +313,7 @@ const unionRefs = enable => {
289
313
 
290
314
  // arithmetic and string operations
291
315
  postfix('%'); // percent
292
- postfix('#', function (left) {
316
+ postfix('#', function (left) { // spilled range (_xlfn.ANCHORARRAY)
293
317
  if (!isReferenceNode(left)) {
294
318
  halt('# expects a reference');
295
319
  }
@@ -340,6 +364,18 @@ symbol(LITERAL).nud = function () {
340
364
  return this;
341
365
  };
342
366
  symbol(REFERENCE).nud = function () {
367
+ if (this.type === REF_NAMED) {
368
+ this.kind = 'name';
369
+ }
370
+ else if (this.type === REF_STRUCT) {
371
+ this.kind = 'table'; // structured ?
372
+ }
373
+ else if (this.type === REF_BEAM) {
374
+ this.kind = 'beam';
375
+ }
376
+ else {
377
+ this.kind = 'range';
378
+ }
343
379
  this.type = REFERENCE;
344
380
  return this;
345
381
  };
@@ -359,8 +395,35 @@ symbol(FUNCTION).nud = function () {
359
395
  return this;
360
396
  };
361
397
  infix('(', 90, function (left) {
398
+ let callee = {
399
+ type: IDENTIFIER,
400
+ name: left.value
401
+ };
362
402
  if (left.id !== FUNCTION) {
363
- halt('Cannot call a ' + left.type);
403
+ if (
404
+ left.type === LAMBDA ||
405
+ // Excel only allows calls to "names" and ref functions. Since we don't
406
+ // differentiate between the two (this requires a table of function names)
407
+ // we're overly permissive here:
408
+ left.type === CALL ||
409
+ left.type === LET ||
410
+ left.type === REFERENCE ||
411
+ (left.type === UNARY && left.value === '#') || // Because it's really SINGLE(...)()
412
+ (left.type === ERROR_LITERAL && left.value === '#REF!')
413
+ ) {
414
+ // in the case of REFERENCE, do we want to set the node to Identifier?
415
+ callee = left;
416
+ }
417
+ else {
418
+ halt('Unexpected call', tokenIndex - 1);
419
+ }
420
+ }
421
+ const lcFn = left.value.toLowerCase();
422
+ if (lcFn === 'lambda') {
423
+ return parseLambda.call(this, left);
424
+ }
425
+ if (lcFn === 'let') {
426
+ return parseLet.call(this, left);
364
427
  }
365
428
  const args = [];
366
429
  let lastWasComma = false;
@@ -393,10 +456,7 @@ infix('(', 90, function (left) {
393
456
  const closeParen = currentNode;
394
457
  delete this.value;
395
458
  this.type = CALL;
396
- this.callee = {
397
- type: IDENTIFIER,
398
- name: left.value
399
- };
459
+ this.callee = callee;
400
460
  if (left.loc) {
401
461
  this.callee.loc = [ ...left.loc ];
402
462
  }
@@ -408,6 +468,149 @@ infix('(', 90, function (left) {
408
468
  return this;
409
469
  });
410
470
 
471
+ function parseLambda (left) {
472
+ const args = [];
473
+ const argNames = {};
474
+ let body;
475
+ let done = false;
476
+ const prevState = unionRefs(false);
477
+ if (currentNode.id !== ')') {
478
+ while (!done) {
479
+ if (isWhitespace(currentNode)) {
480
+ advance();
481
+ }
482
+ const argTokenIndex = tokenIndex;
483
+ const arg = expression(0);
484
+ if (currentNode.id === ',') {
485
+ // all but last args must be names
486
+ if (arg.type === REFERENCE && arg.kind === 'name') {
487
+ // names may not be duplicates
488
+ const currName = arg.value.toLowerCase();
489
+ if (currName in argNames) {
490
+ halt('Duplicate name: ' + arg.value);
491
+ }
492
+ argNames[currName] = 1;
493
+ const a = { type: IDENTIFIER, name: arg.value };
494
+ if (arg.loc) { a.loc = arg.loc; }
495
+ args.push(a);
496
+ }
497
+ else {
498
+ tokenIndex = argTokenIndex;
499
+ halt('LAMBDA argument is not a name');
500
+ }
501
+ advance(',');
502
+ }
503
+ else {
504
+ body = arg;
505
+ done = true;
506
+ }
507
+ }
508
+ }
509
+ unionRefs(prevState);
510
+ delete this.value;
511
+ this.type = LAMBDA;
512
+ this.params = args;
513
+ this.body = body || null;
514
+ if (left.loc) {
515
+ this.loc = [ left.loc[0], currentNode.loc[1] ];
516
+ }
517
+ advance(')', this);
518
+ return this;
519
+ }
520
+
521
+ function parseLet (left) {
522
+ const args = [];
523
+ const vals = [];
524
+ const argNames = {};
525
+ let body;
526
+ let argCounter = 0;
527
+ const addArgument = (arg, lastArg) => {
528
+ if (body) {
529
+ halt('Unexpected argument following calculation');
530
+ }
531
+ if (lastArg && argCounter >= 2) {
532
+ body = arg;
533
+ }
534
+ else {
535
+ const wantName = !(argCounter % 2);
536
+ if (wantName) {
537
+ if (arg && (arg.type === REFERENCE && arg.kind === 'name')) {
538
+ // names may not be duplicates
539
+ const currName = arg.value.toLowerCase();
540
+ if (currName in argNames) {
541
+ halt('Duplicate name: ' + arg.value);
542
+ }
543
+ argNames[currName] = 1;
544
+ args.push({ type: IDENTIFIER, name: arg.value, loc: arg.loc });
545
+ }
546
+ else if (argCounter >= 2) {
547
+ body = arg;
548
+ }
549
+ else {
550
+ halt('Argument is not a name');
551
+ }
552
+ }
553
+ else {
554
+ vals.push(arg);
555
+ }
556
+ }
557
+ argCounter++;
558
+ };
559
+ const prevState = unionRefs(false);
560
+ let lastWasComma = false;
561
+ if (currentNode.id !== ')') {
562
+ while (currentNode.id !== ')') {
563
+ if (isWhitespace(currentNode)) {
564
+ advance();
565
+ }
566
+ if (currentNode.id === ',') {
567
+ addArgument(null);
568
+ lastWasComma = true;
569
+ advance();
570
+ }
571
+ else {
572
+ const arg = expression(0);
573
+ addArgument(arg, currentNode.id !== ',');
574
+ lastWasComma = false;
575
+ if (currentNode.id === ',') {
576
+ advance(',');
577
+ lastWasComma = true;
578
+ }
579
+ }
580
+ }
581
+ unionRefs(prevState);
582
+ }
583
+ if (lastWasComma) {
584
+ addArgument(null, true);
585
+ }
586
+ // eslint-disable-next-line no-undefined
587
+ if (body === undefined) {
588
+ halt('Unexpected end of arguments');
589
+ }
590
+ unionRefs(prevState);
591
+ delete this.value;
592
+ this.type = LET;
593
+ this.declarations = [];
594
+ if (!args.length) {
595
+ halt('Unexpected end of arguments');
596
+ }
597
+ for (let i = 0; i < args.length; i++) {
598
+ const s = {
599
+ type: LET_DECL,
600
+ id: args[i],
601
+ init: vals[i],
602
+ loc: args[i].loc && [ args[i].loc[0], vals[i].loc[1] ]
603
+ };
604
+ this.declarations.push(s);
605
+ }
606
+ this.body = body;
607
+ if (left.loc) {
608
+ this.loc = [ left.loc[0], currentNode.loc[1] ];
609
+ }
610
+ advance(')', this);
611
+ return this;
612
+ }
613
+
411
614
  // array literal
412
615
  symbol('}');
413
616
  symbol(';');
@@ -437,7 +640,6 @@ prefix('{', function () {
437
640
  else if (permitArrayCalls && isFunction(currentNode)) {
438
641
  const arg = expression(0);
439
642
  row.push(arg);
440
- // FIXME: need to skip WS here?
441
643
  }
442
644
  else {
443
645
  halt(`Unexpected ${currentNode.type} in array: ${currentNode.value}`);
@@ -478,7 +680,7 @@ prefix('{', function () {
478
680
  * [AST_format.md](./AST_format.md)
479
681
  *
480
682
  * @see nodeTypes
481
- * @param {(string | Array<Token>)} formula An Excel formula string (an Excel expression) or an array of tokens.
683
+ * @param {(string | Token[])} formula An Excel formula string (an Excel expression) or an array of tokens.
482
684
  * @param {object} [options={}] Options
483
685
  * @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
484
686
  * @param {boolean} [options.allowTernary=false] Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported by Google Sheets but not Excel. See: References.md.
@@ -488,7 +690,7 @@ prefix('{', function () {
488
690
  * @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
489
691
  * @param {boolean} [options.withLocation=false] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
490
692
  * @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
491
- * @returns {object} An AST of nodes
693
+ * @returns {AstExpression} An AST of nodes
492
694
  */
493
695
  export function parse (formula, options) {
494
696
  if (typeof formula === 'string') {