@webmate-studio/builder 0.2.110 → 0.2.113

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,1234 @@
1
+ /**
2
+ * Expression Evaluator - Safe AST-based Expression Parser
3
+ *
4
+ * Universal (isomorphic) module for Browser and Node.js.
5
+ * Supports a safe subset of JavaScript expressions:
6
+ *
7
+ * - Math: +, -, *, /, %
8
+ * - Comparison: >, <, >=, <=, ===, !==, ==, !=
9
+ * - Logical: &&, ||, !
10
+ * - Ternary: condition ? a : b
11
+ * - Nullish coalescing: a ?? b
12
+ * - Property access: obj.prop, obj?.prop, obj[index]
13
+ * - Function calls (whitelisted): items.filter(x => x.active)
14
+ * - Arrow functions: (x) => x.active, x => x + 1
15
+ * - Array literals: [1, 2, 3]
16
+ * - Object literals: { key: value }
17
+ * - Template literals: `hello ${name}`
18
+ * - Spread: ...items
19
+ * - typeof operator
20
+ *
21
+ * Security:
22
+ * - No eval(), no Function constructor
23
+ * - No access to window, document, globalThis, process
24
+ * - No assignments (=, +=, etc.)
25
+ * - No import/require
26
+ * - Method calls only on whitelisted prototypes
27
+ *
28
+ * @module expression-evaluator
29
+ */
30
+
31
+ // ============================================================
32
+ // Token Types
33
+ // ============================================================
34
+
35
+ const T = {
36
+ NUMBER: 1,
37
+ STRING: 2,
38
+ IDENTIFIER: 3,
39
+ TRUE: 4,
40
+ FALSE: 5,
41
+ NULL: 6,
42
+ UNDEFINED: 7,
43
+ PLUS: 8,
44
+ MINUS: 9,
45
+ STAR: 10,
46
+ SLASH: 11,
47
+ PERCENT: 12,
48
+ GT: 13,
49
+ LT: 14,
50
+ GTE: 15,
51
+ LTE: 16,
52
+ EQ_STRICT: 17,
53
+ NEQ_STRICT: 18,
54
+ EQ_LOOSE: 19,
55
+ NEQ_LOOSE: 20,
56
+ AND: 21,
57
+ OR: 22,
58
+ NOT: 23,
59
+ QUESTION: 24,
60
+ COLON: 25,
61
+ DOT: 26,
62
+ OPTIONAL_CHAIN: 27,
63
+ LPAREN: 28,
64
+ RPAREN: 29,
65
+ LBRACKET: 30,
66
+ RBRACKET: 31,
67
+ LBRACE: 32,
68
+ RBRACE: 33,
69
+ COMMA: 34,
70
+ ARROW: 35,
71
+ SPREAD: 36,
72
+ TYPEOF: 37,
73
+ NULLISH: 38,
74
+ TEMPLATE: 39,
75
+ EOF: 40,
76
+ };
77
+
78
+ // ============================================================
79
+ // Whitelisted methods for safe execution
80
+ // ============================================================
81
+
82
+ const ALLOWED_ARRAY_METHODS = new Set([
83
+ 'filter', 'map', 'find', 'findIndex', 'some', 'every',
84
+ 'includes', 'indexOf', 'lastIndexOf',
85
+ 'slice', 'concat', 'flat', 'flatMap',
86
+ 'join', 'reverse', 'sort',
87
+ 'reduce', 'reduceRight',
88
+ 'fill', 'from', 'isArray', 'of',
89
+ 'at', 'entries', 'keys', 'values',
90
+ 'forEach',
91
+ ]);
92
+
93
+ const ALLOWED_STRING_METHODS = new Set([
94
+ 'toLowerCase', 'toUpperCase', 'trim', 'trimStart', 'trimEnd',
95
+ 'startsWith', 'endsWith', 'includes', 'indexOf', 'lastIndexOf',
96
+ 'split', 'replace', 'replaceAll', 'substring', 'slice',
97
+ 'charAt', 'charCodeAt', 'padStart', 'padEnd', 'repeat',
98
+ 'match', 'search', 'at',
99
+ ]);
100
+
101
+ const ALLOWED_NUMBER_METHODS = new Set([
102
+ 'toFixed', 'toPrecision', 'toString',
103
+ ]);
104
+
105
+ const ALLOWED_OBJECT_STATIC = new Set([
106
+ 'keys', 'values', 'entries', 'assign', 'fromEntries',
107
+ ]);
108
+
109
+ const ALLOWED_MATH_PROPS = new Set([
110
+ 'floor', 'ceil', 'round', 'abs', 'min', 'max',
111
+ 'pow', 'sqrt', 'sign', 'trunc', 'random',
112
+ 'PI', 'E',
113
+ ]);
114
+
115
+ const ALLOWED_JSON_METHODS = new Set([
116
+ 'stringify', 'parse',
117
+ ]);
118
+
119
+ const ALLOWED_GLOBALS = new Set([
120
+ 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
121
+ 'Number', 'String', 'Boolean', 'Array', 'Object',
122
+ 'Math', 'JSON', 'Date', 'console',
123
+ ]);
124
+
125
+ const BLOCKED_IDENTIFIERS = new Set([
126
+ 'window', 'document', 'globalThis', 'self',
127
+ 'process', 'require', 'import', 'module', 'exports',
128
+ 'eval', 'Function', 'setTimeout', 'setInterval',
129
+ 'fetch', 'XMLHttpRequest', 'WebSocket',
130
+ '__proto__', 'constructor', 'prototype',
131
+ ]);
132
+
133
+ // ============================================================
134
+ // Lexer
135
+ // ============================================================
136
+
137
+ class Lexer {
138
+ constructor(input) {
139
+ this.input = input;
140
+ this.pos = 0;
141
+ this.length = input.length;
142
+ }
143
+
144
+ peek(offset = 0) {
145
+ const p = this.pos + offset;
146
+ return p < this.length ? this.input[p] : null;
147
+ }
148
+
149
+ advance(n = 1) {
150
+ this.pos += n;
151
+ }
152
+
153
+ skipWhitespace() {
154
+ while (this.pos < this.length && /\s/.test(this.input[this.pos])) {
155
+ this.pos++;
156
+ }
157
+ }
158
+
159
+ readNumber() {
160
+ const start = this.pos;
161
+ // Handle hex: 0x...
162
+ if (this.input[this.pos] === '0' && (this.input[this.pos + 1] === 'x' || this.input[this.pos + 1] === 'X')) {
163
+ this.pos += 2;
164
+ while (this.pos < this.length && /[0-9a-fA-F]/.test(this.input[this.pos])) this.pos++;
165
+ return { type: T.NUMBER, value: parseInt(this.input.slice(start, this.pos), 16) };
166
+ }
167
+ while (this.pos < this.length && /[0-9]/.test(this.input[this.pos])) this.pos++;
168
+ if (this.pos < this.length && this.input[this.pos] === '.' && /[0-9]/.test(this.input[this.pos + 1] || '')) {
169
+ this.pos++;
170
+ while (this.pos < this.length && /[0-9]/.test(this.input[this.pos])) this.pos++;
171
+ }
172
+ // Scientific notation
173
+ if (this.pos < this.length && (this.input[this.pos] === 'e' || this.input[this.pos] === 'E')) {
174
+ this.pos++;
175
+ if (this.pos < this.length && (this.input[this.pos] === '+' || this.input[this.pos] === '-')) this.pos++;
176
+ while (this.pos < this.length && /[0-9]/.test(this.input[this.pos])) this.pos++;
177
+ }
178
+ return { type: T.NUMBER, value: parseFloat(this.input.slice(start, this.pos)) };
179
+ }
180
+
181
+ readString(quote) {
182
+ this.pos++; // skip opening quote
183
+ let str = '';
184
+ while (this.pos < this.length && this.input[this.pos] !== quote) {
185
+ if (this.input[this.pos] === '\\') {
186
+ this.pos++;
187
+ if (this.pos >= this.length) break;
188
+ const c = this.input[this.pos];
189
+ switch (c) {
190
+ case 'n': str += '\n'; break;
191
+ case 't': str += '\t'; break;
192
+ case 'r': str += '\r'; break;
193
+ case '\\': str += '\\'; break;
194
+ case "'": str += "'"; break;
195
+ case '"': str += '"'; break;
196
+ case '`': str += '`'; break;
197
+ default: str += c;
198
+ }
199
+ } else {
200
+ str += this.input[this.pos];
201
+ }
202
+ this.pos++;
203
+ }
204
+ this.pos++; // skip closing quote
205
+ return str;
206
+ }
207
+
208
+ readTemplateLiteral() {
209
+ this.pos++; // skip opening backtick
210
+ const parts = []; // Array of { type: 'text', value } or { type: 'expr', value }
211
+ let text = '';
212
+
213
+ while (this.pos < this.length && this.input[this.pos] !== '`') {
214
+ if (this.input[this.pos] === '\\') {
215
+ this.pos++;
216
+ if (this.pos >= this.length) break;
217
+ const c = this.input[this.pos];
218
+ switch (c) {
219
+ case 'n': text += '\n'; break;
220
+ case 't': text += '\t'; break;
221
+ case '`': text += '`'; break;
222
+ case '$': text += '$'; break;
223
+ case '\\': text += '\\'; break;
224
+ default: text += c;
225
+ }
226
+ this.pos++;
227
+ } else if (this.input[this.pos] === '$' && this.input[this.pos + 1] === '{') {
228
+ // Template expression
229
+ if (text) {
230
+ parts.push({ type: 'text', value: text });
231
+ text = '';
232
+ }
233
+ this.pos += 2; // skip ${
234
+
235
+ // Find matching } (handle nesting)
236
+ let depth = 1;
237
+ let exprStart = this.pos;
238
+ while (this.pos < this.length && depth > 0) {
239
+ if (this.input[this.pos] === '{') depth++;
240
+ else if (this.input[this.pos] === '}') depth--;
241
+ if (depth > 0) this.pos++;
242
+ }
243
+ parts.push({ type: 'expr', value: this.input.slice(exprStart, this.pos) });
244
+ this.pos++; // skip }
245
+ } else {
246
+ text += this.input[this.pos];
247
+ this.pos++;
248
+ }
249
+ }
250
+ this.pos++; // skip closing backtick
251
+
252
+ if (text) {
253
+ parts.push({ type: 'text', value: text });
254
+ }
255
+
256
+ return { type: T.TEMPLATE, parts };
257
+ }
258
+
259
+ readIdentifier() {
260
+ const start = this.pos;
261
+ while (this.pos < this.length && /[a-zA-Z0-9_$]/.test(this.input[this.pos])) {
262
+ this.pos++;
263
+ }
264
+ return this.input.slice(start, this.pos);
265
+ }
266
+
267
+ getNextToken() {
268
+ this.skipWhitespace();
269
+ if (this.pos >= this.length) return { type: T.EOF };
270
+
271
+ const ch = this.input[this.pos];
272
+ const next = this.pos + 1 < this.length ? this.input[this.pos + 1] : null;
273
+ const next2 = this.pos + 2 < this.length ? this.input[this.pos + 2] : null;
274
+
275
+ // Numbers
276
+ if (/[0-9]/.test(ch)) return this.readNumber();
277
+
278
+ // Strings
279
+ if (ch === '"' || ch === "'") return { type: T.STRING, value: this.readString(ch) };
280
+
281
+ // Template literals
282
+ if (ch === '`') return this.readTemplateLiteral();
283
+
284
+ // Spread operator
285
+ if (ch === '.' && next === '.' && next2 === '.') {
286
+ this.advance(3);
287
+ return { type: T.SPREAD };
288
+ }
289
+
290
+ // Identifiers and keywords
291
+ if (/[a-zA-Z_$]/.test(ch)) {
292
+ const id = this.readIdentifier();
293
+ switch (id) {
294
+ case 'true': return { type: T.TRUE, value: true };
295
+ case 'false': return { type: T.FALSE, value: false };
296
+ case 'null': return { type: T.NULL, value: null };
297
+ case 'undefined': return { type: T.UNDEFINED, value: undefined };
298
+ case 'typeof': return { type: T.TYPEOF };
299
+ default: return { type: T.IDENTIFIER, value: id };
300
+ }
301
+ }
302
+
303
+ // Multi-character operators
304
+ switch (ch) {
305
+ case '=':
306
+ if (next === '=' && next2 === '=') { this.advance(3); return { type: T.EQ_STRICT }; }
307
+ if (next === '=') { this.advance(2); return { type: T.EQ_LOOSE }; }
308
+ if (next === '>') { this.advance(2); return { type: T.ARROW }; }
309
+ throw new Error(`Assignment operator = not allowed in expressions`);
310
+ case '!':
311
+ if (next === '=' && next2 === '=') { this.advance(3); return { type: T.NEQ_STRICT }; }
312
+ if (next === '=') { this.advance(2); return { type: T.NEQ_LOOSE }; }
313
+ this.advance(); return { type: T.NOT };
314
+ case '>':
315
+ if (next === '=') { this.advance(2); return { type: T.GTE }; }
316
+ this.advance(); return { type: T.GT };
317
+ case '<':
318
+ if (next === '=') { this.advance(2); return { type: T.LTE }; }
319
+ this.advance(); return { type: T.LT };
320
+ case '&':
321
+ if (next === '&') { this.advance(2); return { type: T.AND }; }
322
+ throw new Error('Bitwise & not supported, use &&');
323
+ case '|':
324
+ if (next === '|') { this.advance(2); return { type: T.OR }; }
325
+ throw new Error('Bitwise | not supported, use ||');
326
+ case '?':
327
+ if (next === '?') { this.advance(2); return { type: T.NULLISH }; }
328
+ if (next === '.') { this.advance(2); return { type: T.OPTIONAL_CHAIN }; }
329
+ this.advance(); return { type: T.QUESTION };
330
+ case '+': this.advance(); return { type: T.PLUS };
331
+ case '-': this.advance(); return { type: T.MINUS };
332
+ case '*': this.advance(); return { type: T.STAR };
333
+ case '/': this.advance(); return { type: T.SLASH };
334
+ case '%': this.advance(); return { type: T.PERCENT };
335
+ case '(': this.advance(); return { type: T.LPAREN };
336
+ case ')': this.advance(); return { type: T.RPAREN };
337
+ case '[': this.advance(); return { type: T.LBRACKET };
338
+ case ']': this.advance(); return { type: T.RBRACKET };
339
+ case '{': this.advance(); return { type: T.LBRACE };
340
+ case '}': this.advance(); return { type: T.RBRACE };
341
+ case ',': this.advance(); return { type: T.COMMA };
342
+ case '.': this.advance(); return { type: T.DOT };
343
+ case ':': this.advance(); return { type: T.COLON };
344
+ }
345
+
346
+ throw new Error(`Unexpected character: ${ch} at position ${this.pos}`);
347
+ }
348
+ }
349
+
350
+ // ============================================================
351
+ // Parser — Builds AST from token stream
352
+ // ============================================================
353
+
354
+ class Parser {
355
+ constructor(lexer) {
356
+ this.lexer = lexer;
357
+ this.current = this.lexer.getNextToken();
358
+ }
359
+
360
+ eat(type) {
361
+ if (this.current.type === type) {
362
+ const token = this.current;
363
+ this.current = this.lexer.getNextToken();
364
+ return token;
365
+ }
366
+ throw new Error(`Expected token type ${type}, got ${this.current.type}`);
367
+ }
368
+
369
+ // Entry point
370
+ parse() {
371
+ const node = this.expression();
372
+ if (this.current.type !== T.EOF) {
373
+ throw new Error(`Unexpected token after expression`);
374
+ }
375
+ return node;
376
+ }
377
+
378
+ // expression: assignment-level (we don't allow assignment, so this is ternary/nullish)
379
+ expression() {
380
+ return this.ternary();
381
+ }
382
+
383
+ // ternary: nullishCoalescing (? expression : expression)?
384
+ ternary() {
385
+ let node = this.nullishCoalescing();
386
+
387
+ if (this.current.type === T.QUESTION) {
388
+ this.eat(T.QUESTION);
389
+ const consequent = this.expression();
390
+ this.eat(T.COLON);
391
+ const alternate = this.expression();
392
+ return { type: 'Conditional', test: node, consequent, alternate };
393
+ }
394
+
395
+ return node;
396
+ }
397
+
398
+ // nullishCoalescing: logicalOr (?? logicalOr)*
399
+ nullishCoalescing() {
400
+ let node = this.logicalOr();
401
+
402
+ while (this.current.type === T.NULLISH) {
403
+ this.eat(T.NULLISH);
404
+ node = { type: 'Nullish', left: node, right: this.logicalOr() };
405
+ }
406
+
407
+ return node;
408
+ }
409
+
410
+ // logicalOr: logicalAnd (|| logicalAnd)*
411
+ logicalOr() {
412
+ let node = this.logicalAnd();
413
+
414
+ while (this.current.type === T.OR) {
415
+ this.eat(T.OR);
416
+ node = { type: 'Logical', op: '||', left: node, right: this.logicalAnd() };
417
+ }
418
+
419
+ return node;
420
+ }
421
+
422
+ // logicalAnd: comparison (&& comparison)*
423
+ logicalAnd() {
424
+ let node = this.comparison();
425
+
426
+ while (this.current.type === T.AND) {
427
+ this.eat(T.AND);
428
+ node = { type: 'Logical', op: '&&', left: node, right: this.comparison() };
429
+ }
430
+
431
+ return node;
432
+ }
433
+
434
+ // comparison: additive (comp_op additive)*
435
+ comparison() {
436
+ let node = this.additive();
437
+
438
+ const compTypes = [T.GT, T.LT, T.GTE, T.LTE, T.EQ_STRICT, T.NEQ_STRICT, T.EQ_LOOSE, T.NEQ_LOOSE];
439
+ while (compTypes.includes(this.current.type)) {
440
+ const opMap = {
441
+ [T.GT]: '>', [T.LT]: '<', [T.GTE]: '>=', [T.LTE]: '<=',
442
+ [T.EQ_STRICT]: '===', [T.NEQ_STRICT]: '!==',
443
+ [T.EQ_LOOSE]: '==', [T.NEQ_LOOSE]: '!=',
444
+ };
445
+ const op = opMap[this.current.type];
446
+ this.eat(this.current.type);
447
+ node = { type: 'Binary', op, left: node, right: this.additive() };
448
+ }
449
+
450
+ return node;
451
+ }
452
+
453
+ // additive: multiplicative ((+|-) multiplicative)*
454
+ additive() {
455
+ let node = this.multiplicative();
456
+
457
+ while (this.current.type === T.PLUS || this.current.type === T.MINUS) {
458
+ const op = this.current.type === T.PLUS ? '+' : '-';
459
+ this.eat(this.current.type);
460
+ node = { type: 'Binary', op, left: node, right: this.multiplicative() };
461
+ }
462
+
463
+ return node;
464
+ }
465
+
466
+ // multiplicative: unary ((*|/|%) unary)*
467
+ multiplicative() {
468
+ let node = this.unary();
469
+
470
+ while (this.current.type === T.STAR || this.current.type === T.SLASH || this.current.type === T.PERCENT) {
471
+ const opMap = { [T.STAR]: '*', [T.SLASH]: '/', [T.PERCENT]: '%' };
472
+ const op = opMap[this.current.type];
473
+ this.eat(this.current.type);
474
+ node = { type: 'Binary', op, left: node, right: this.unary() };
475
+ }
476
+
477
+ return node;
478
+ }
479
+
480
+ // unary: (!|+|-|typeof|...) unary | postfix
481
+ unary() {
482
+ if (this.current.type === T.NOT) {
483
+ this.eat(T.NOT);
484
+ return { type: 'Unary', op: '!', argument: this.unary() };
485
+ }
486
+ if (this.current.type === T.MINUS) {
487
+ this.eat(T.MINUS);
488
+ return { type: 'Unary', op: '-', argument: this.unary() };
489
+ }
490
+ if (this.current.type === T.PLUS) {
491
+ this.eat(T.PLUS);
492
+ return { type: 'Unary', op: '+', argument: this.unary() };
493
+ }
494
+ if (this.current.type === T.TYPEOF) {
495
+ this.eat(T.TYPEOF);
496
+ return { type: 'Typeof', argument: this.unary() };
497
+ }
498
+ if (this.current.type === T.SPREAD) {
499
+ this.eat(T.SPREAD);
500
+ return { type: 'Spread', argument: this.unary() };
501
+ }
502
+
503
+ return this.callOrMember();
504
+ }
505
+
506
+ // callOrMember: primary (. prop | ?. prop | [expr] | (args))*
507
+ callOrMember() {
508
+ let node = this.primary();
509
+
510
+ while (true) {
511
+ if (this.current.type === T.DOT) {
512
+ this.eat(T.DOT);
513
+ const prop = this.eat(T.IDENTIFIER).value;
514
+ node = { type: 'Member', object: node, property: prop, computed: false, optional: false };
515
+ } else if (this.current.type === T.OPTIONAL_CHAIN) {
516
+ this.eat(T.OPTIONAL_CHAIN);
517
+ if (this.current.type === T.LBRACKET) {
518
+ // ?.[ computed access
519
+ this.eat(T.LBRACKET);
520
+ const expr = this.expression();
521
+ this.eat(T.RBRACKET);
522
+ node = { type: 'Member', object: node, property: expr, computed: true, optional: true };
523
+ } else if (this.current.type === T.LPAREN) {
524
+ // ?.( optional call
525
+ const args = this.parseArguments();
526
+ node = { type: 'Call', callee: node, arguments: args, optional: true };
527
+ } else {
528
+ const prop = this.eat(T.IDENTIFIER).value;
529
+ node = { type: 'Member', object: node, property: prop, computed: false, optional: true };
530
+ }
531
+ } else if (this.current.type === T.LBRACKET) {
532
+ this.eat(T.LBRACKET);
533
+ const expr = this.expression();
534
+ this.eat(T.RBRACKET);
535
+ node = { type: 'Member', object: node, property: expr, computed: true, optional: false };
536
+ } else if (this.current.type === T.LPAREN) {
537
+ const args = this.parseArguments();
538
+ node = { type: 'Call', callee: node, arguments: args, optional: false };
539
+ } else {
540
+ break;
541
+ }
542
+ }
543
+
544
+ return node;
545
+ }
546
+
547
+ parseArguments() {
548
+ this.eat(T.LPAREN);
549
+ const args = [];
550
+ while (this.current.type !== T.RPAREN) {
551
+ if (this.current.type === T.SPREAD) {
552
+ this.eat(T.SPREAD);
553
+ args.push({ type: 'Spread', argument: this.expression() });
554
+ } else {
555
+ args.push(this.expression());
556
+ }
557
+ if (this.current.type === T.COMMA) {
558
+ this.eat(T.COMMA);
559
+ }
560
+ }
561
+ this.eat(T.RPAREN);
562
+ return args;
563
+ }
564
+
565
+ // primary: literals, identifiers, grouped, arrays, objects, arrow functions
566
+ primary() {
567
+ const tok = this.current;
568
+
569
+ // Number
570
+ if (tok.type === T.NUMBER) {
571
+ this.eat(T.NUMBER);
572
+ return { type: 'Literal', value: tok.value };
573
+ }
574
+
575
+ // String
576
+ if (tok.type === T.STRING) {
577
+ this.eat(T.STRING);
578
+ return { type: 'Literal', value: tok.value };
579
+ }
580
+
581
+ // Template literal
582
+ if (tok.type === T.TEMPLATE) {
583
+ this.eat(T.TEMPLATE);
584
+ return { type: 'TemplateLiteral', parts: tok.parts };
585
+ }
586
+
587
+ // Boolean / null / undefined
588
+ if (tok.type === T.TRUE || tok.type === T.FALSE) {
589
+ this.eat(tok.type);
590
+ return { type: 'Literal', value: tok.value };
591
+ }
592
+ if (tok.type === T.NULL) {
593
+ this.eat(T.NULL);
594
+ return { type: 'Literal', value: null };
595
+ }
596
+ if (tok.type === T.UNDEFINED) {
597
+ this.eat(T.UNDEFINED);
598
+ return { type: 'Literal', value: undefined };
599
+ }
600
+
601
+ // Identifier (might be start of arrow function: ident => ...)
602
+ if (tok.type === T.IDENTIFIER) {
603
+ this.eat(T.IDENTIFIER);
604
+ // Check for arrow function: ident => body
605
+ if (this.current.type === T.ARROW) {
606
+ this.eat(T.ARROW);
607
+ const body = this.expression();
608
+ return { type: 'Arrow', params: [tok.value], body };
609
+ }
610
+ return { type: 'Identifier', name: tok.value };
611
+ }
612
+
613
+ // Grouped expression or arrow function params: (a, b) => ...
614
+ if (tok.type === T.LPAREN) {
615
+ return this.parseParenOrArrow();
616
+ }
617
+
618
+ // Array literal: [a, b, c]
619
+ if (tok.type === T.LBRACKET) {
620
+ return this.parseArray();
621
+ }
622
+
623
+ // Object literal: { key: value }
624
+ if (tok.type === T.LBRACE) {
625
+ return this.parseObject();
626
+ }
627
+
628
+ throw new Error(`Unexpected token: ${tok.type} (value: ${JSON.stringify(tok.value)})`);
629
+ }
630
+
631
+ parseParenOrArrow() {
632
+ this.eat(T.LPAREN);
633
+
634
+ // Empty parens: () => ...
635
+ if (this.current.type === T.RPAREN) {
636
+ this.eat(T.RPAREN);
637
+ this.eat(T.ARROW);
638
+ const body = this.expression();
639
+ return { type: 'Arrow', params: [], body };
640
+ }
641
+
642
+ // Try to parse as arrow function params
643
+ // Save position for backtracking
644
+ const savedPos = this.lexer.pos;
645
+ const savedCurrent = { ...this.current };
646
+
647
+ // Collect potential params
648
+ const params = [];
649
+ let isArrow = true;
650
+ let firstExpr = null;
651
+
652
+ try {
653
+ // First item
654
+ if (this.current.type === T.SPREAD) {
655
+ // Rest parameter: (...args) => ...
656
+ this.eat(T.SPREAD);
657
+ params.push({ type: 'rest', name: this.eat(T.IDENTIFIER).value });
658
+ } else if (this.current.type === T.IDENTIFIER) {
659
+ params.push(this.current.value);
660
+ this.eat(T.IDENTIFIER);
661
+ } else {
662
+ // Not a simple param list — it's a grouped expression
663
+ isArrow = false;
664
+ }
665
+
666
+ if (isArrow) {
667
+ while (this.current.type === T.COMMA) {
668
+ this.eat(T.COMMA);
669
+ if (this.current.type === T.SPREAD) {
670
+ this.eat(T.SPREAD);
671
+ params.push({ type: 'rest', name: this.eat(T.IDENTIFIER).value });
672
+ } else if (this.current.type === T.IDENTIFIER) {
673
+ params.push(this.current.value);
674
+ this.eat(T.IDENTIFIER);
675
+ } else {
676
+ isArrow = false;
677
+ break;
678
+ }
679
+ }
680
+ }
681
+
682
+ if (isArrow && this.current.type === T.RPAREN) {
683
+ this.eat(T.RPAREN);
684
+ if (this.current.type === T.ARROW) {
685
+ this.eat(T.ARROW);
686
+ const body = this.expression();
687
+ return { type: 'Arrow', params: params.map(p => typeof p === 'string' ? p : p), body };
688
+ }
689
+ // It was (identifier) but no arrow — treat as grouped expression
690
+ if (params.length === 1 && typeof params[0] === 'string') {
691
+ return { type: 'Identifier', name: params[0] };
692
+ }
693
+ }
694
+ } catch (e) {
695
+ isArrow = false;
696
+ }
697
+
698
+ // Backtrack and parse as grouped expression
699
+ if (!isArrow || (isArrow && this.current.type !== T.EOF)) {
700
+ // We can't easily backtrack with our lexer, so re-lex
701
+ this.lexer.pos = savedPos;
702
+ this.current = savedCurrent;
703
+
704
+ // It's just a regular grouped expression if we get here with a single non-arrow identifier
705
+ // But we already consumed LPAREN, so parse the expression
706
+ }
707
+
708
+ // Parse as grouped expression
709
+ const expr = this.expression();
710
+ this.eat(T.RPAREN);
711
+
712
+ // Check if this is actually arrow: (expr) =>
713
+ if (this.current.type === T.ARROW) {
714
+ this.eat(T.ARROW);
715
+ const body = this.expression();
716
+ // expr should be an Identifier
717
+ if (expr.type === 'Identifier') {
718
+ return { type: 'Arrow', params: [expr.name], body };
719
+ }
720
+ }
721
+
722
+ return expr;
723
+ }
724
+
725
+ parseArray() {
726
+ this.eat(T.LBRACKET);
727
+ const elements = [];
728
+ while (this.current.type !== T.RBRACKET) {
729
+ if (this.current.type === T.SPREAD) {
730
+ this.eat(T.SPREAD);
731
+ elements.push({ type: 'Spread', argument: this.expression() });
732
+ } else {
733
+ elements.push(this.expression());
734
+ }
735
+ if (this.current.type === T.COMMA) {
736
+ this.eat(T.COMMA);
737
+ }
738
+ }
739
+ this.eat(T.RBRACKET);
740
+ return { type: 'ArrayLiteral', elements };
741
+ }
742
+
743
+ parseObject() {
744
+ this.eat(T.LBRACE);
745
+ const properties = [];
746
+ while (this.current.type !== T.RBRACE) {
747
+ if (this.current.type === T.SPREAD) {
748
+ this.eat(T.SPREAD);
749
+ properties.push({ type: 'SpreadProperty', argument: this.expression() });
750
+ } else {
751
+ let key;
752
+ let computed = false;
753
+
754
+ if (this.current.type === T.LBRACKET) {
755
+ // Computed key: { [expr]: value }
756
+ this.eat(T.LBRACKET);
757
+ key = this.expression();
758
+ this.eat(T.RBRACKET);
759
+ computed = true;
760
+ } else if (this.current.type === T.STRING) {
761
+ key = this.eat(T.STRING).value;
762
+ } else if (this.current.type === T.NUMBER) {
763
+ key = String(this.eat(T.NUMBER).value);
764
+ } else {
765
+ key = this.eat(T.IDENTIFIER).value;
766
+ }
767
+
768
+ if (this.current.type === T.COLON) {
769
+ this.eat(T.COLON);
770
+ const value = this.expression();
771
+ properties.push({ type: 'Property', key, value, computed });
772
+ } else {
773
+ // Shorthand: { foo } is { foo: foo }
774
+ properties.push({
775
+ type: 'Property',
776
+ key,
777
+ value: { type: 'Identifier', name: key },
778
+ computed: false,
779
+ });
780
+ }
781
+ }
782
+ if (this.current.type === T.COMMA) {
783
+ this.eat(T.COMMA);
784
+ }
785
+ }
786
+ this.eat(T.RBRACE);
787
+ return { type: 'ObjectLiteral', properties };
788
+ }
789
+ }
790
+
791
+ // ============================================================
792
+ // Evaluator — Evaluates AST with a context
793
+ // ============================================================
794
+
795
+ class Evaluator {
796
+ constructor(context) {
797
+ this.context = context;
798
+ }
799
+
800
+ evaluate(node) {
801
+ if (!node) return undefined;
802
+
803
+ switch (node.type) {
804
+ case 'Literal':
805
+ return node.value;
806
+
807
+ case 'Identifier':
808
+ return this.resolveIdentifier(node.name);
809
+
810
+ case 'TemplateLiteral':
811
+ return this.evaluateTemplateLiteral(node);
812
+
813
+ case 'Binary':
814
+ return this.evaluateBinary(node);
815
+
816
+ case 'Logical':
817
+ return this.evaluateLogical(node);
818
+
819
+ case 'Unary':
820
+ return this.evaluateUnary(node);
821
+
822
+ case 'Typeof':
823
+ return this.evaluateTypeof(node);
824
+
825
+ case 'Conditional':
826
+ return this.evaluate(node.test) ? this.evaluate(node.consequent) : this.evaluate(node.alternate);
827
+
828
+ case 'Nullish': {
829
+ const left = this.evaluate(node.left);
830
+ return left != null ? left : this.evaluate(node.right);
831
+ }
832
+
833
+ case 'Member':
834
+ return this.evaluateMember(node);
835
+
836
+ case 'Call':
837
+ return this.evaluateCall(node);
838
+
839
+ case 'Arrow':
840
+ return this.evaluateArrow(node);
841
+
842
+ case 'ArrayLiteral':
843
+ return this.evaluateArray(node);
844
+
845
+ case 'ObjectLiteral':
846
+ return this.evaluateObject(node);
847
+
848
+ case 'Spread':
849
+ return this.evaluate(node.argument);
850
+
851
+ default:
852
+ throw new Error(`Unknown AST node type: ${node.type}`);
853
+ }
854
+ }
855
+
856
+ resolveIdentifier(name) {
857
+ if (BLOCKED_IDENTIFIERS.has(name)) {
858
+ throw new Error(`Access to '${name}' is not allowed`);
859
+ }
860
+
861
+ // Built-in globals
862
+ if (name === 'Math') return Math;
863
+ if (name === 'JSON') return JSON;
864
+ if (name === 'parseInt') return parseInt;
865
+ if (name === 'parseFloat') return parseFloat;
866
+ if (name === 'isNaN') return isNaN;
867
+ if (name === 'isFinite') return isFinite;
868
+ if (name === 'Number') return Number;
869
+ if (name === 'String') return String;
870
+ if (name === 'Boolean') return Boolean;
871
+ if (name === 'Array') return Array;
872
+ if (name === 'Object') return Object;
873
+ if (name === 'Date') return Date;
874
+ if (name === 'Infinity') return Infinity;
875
+ if (name === 'NaN') return NaN;
876
+
877
+ // Context lookup
878
+ if (name in this.context) {
879
+ return this.context[name];
880
+ }
881
+
882
+ return undefined;
883
+ }
884
+
885
+ evaluateTemplateLiteral(node) {
886
+ return node.parts.map(part => {
887
+ if (part.type === 'text') return part.value;
888
+ // Parse and evaluate the expression inside ${}
889
+ const innerAst = new Parser(new Lexer(part.value)).parse();
890
+ return String(this.evaluate(innerAst) ?? '');
891
+ }).join('');
892
+ }
893
+
894
+ evaluateBinary(node) {
895
+ const left = this.evaluate(node.left);
896
+ const right = this.evaluate(node.right);
897
+
898
+ switch (node.op) {
899
+ case '+': return left + right;
900
+ case '-': return left - right;
901
+ case '*': return left * right;
902
+ case '/': return left / right;
903
+ case '%': return left % right;
904
+ case '>': return left > right;
905
+ case '<': return left < right;
906
+ case '>=': return left >= right;
907
+ case '<=': return left <= right;
908
+ case '===': return left === right;
909
+ case '!==': return left !== right;
910
+ case '==': return left == right;
911
+ case '!=': return left != right;
912
+ default: throw new Error(`Unknown binary operator: ${node.op}`);
913
+ }
914
+ }
915
+
916
+ evaluateLogical(node) {
917
+ const left = this.evaluate(node.left);
918
+ if (node.op === '&&') return left ? this.evaluate(node.right) : left;
919
+ if (node.op === '||') return left ? left : this.evaluate(node.right);
920
+ throw new Error(`Unknown logical operator: ${node.op}`);
921
+ }
922
+
923
+ evaluateUnary(node) {
924
+ const arg = this.evaluate(node.argument);
925
+ switch (node.op) {
926
+ case '!': return !arg;
927
+ case '-': return -arg;
928
+ case '+': return +arg;
929
+ default: throw new Error(`Unknown unary operator: ${node.op}`);
930
+ }
931
+ }
932
+
933
+ evaluateTypeof(node) {
934
+ try {
935
+ const val = this.evaluate(node.argument);
936
+ return typeof val;
937
+ } catch {
938
+ return 'undefined';
939
+ }
940
+ }
941
+
942
+ evaluateMember(node) {
943
+ const object = this.evaluate(node.object);
944
+
945
+ if (node.optional && (object == null)) {
946
+ return undefined;
947
+ }
948
+
949
+ if (object == null) {
950
+ return undefined;
951
+ }
952
+
953
+ if (node.computed) {
954
+ const key = this.evaluate(node.property);
955
+ return object[key];
956
+ }
957
+
958
+ const prop = node.property;
959
+
960
+ // Block dangerous properties
961
+ if (prop === '__proto__' || prop === 'constructor' || prop === 'prototype') {
962
+ return undefined;
963
+ }
964
+
965
+ return object[prop];
966
+ }
967
+
968
+ evaluateCall(node) {
969
+ // Handle method calls: obj.method(args)
970
+ if (node.callee.type === 'Member') {
971
+ const obj = this.evaluate(node.callee.object);
972
+
973
+ if (node.optional && (obj == null)) {
974
+ return undefined;
975
+ }
976
+
977
+ if (obj == null) {
978
+ return undefined;
979
+ }
980
+
981
+ const methodName = node.callee.computed
982
+ ? this.evaluate(node.callee.property)
983
+ : node.callee.property;
984
+
985
+ // Validate method call is allowed
986
+ this.validateMethodCall(obj, methodName);
987
+
988
+ const method = obj[methodName];
989
+ if (typeof method !== 'function') {
990
+ throw new Error(`${methodName} is not a function`);
991
+ }
992
+
993
+ const args = this.evaluateArgList(node.arguments);
994
+ return method.apply(obj, args);
995
+ }
996
+
997
+ // Handle direct function calls: fn(args) or Array.isArray(x)
998
+ const callee = this.evaluate(node.callee);
999
+
1000
+ if (typeof callee !== 'function') {
1001
+ throw new Error('Not a function');
1002
+ }
1003
+
1004
+ const args = this.evaluateArgList(node.arguments);
1005
+ return callee(...args);
1006
+ }
1007
+
1008
+ validateMethodCall(obj, methodName) {
1009
+ if (Array.isArray(obj)) {
1010
+ if (!ALLOWED_ARRAY_METHODS.has(methodName)) {
1011
+ throw new Error(`Array method '${methodName}' is not allowed`);
1012
+ }
1013
+ return;
1014
+ }
1015
+
1016
+ if (typeof obj === 'string') {
1017
+ if (!ALLOWED_STRING_METHODS.has(methodName)) {
1018
+ throw new Error(`String method '${methodName}' is not allowed`);
1019
+ }
1020
+ return;
1021
+ }
1022
+
1023
+ if (typeof obj === 'number') {
1024
+ if (!ALLOWED_NUMBER_METHODS.has(methodName)) {
1025
+ throw new Error(`Number method '${methodName}' is not allowed`);
1026
+ }
1027
+ return;
1028
+ }
1029
+
1030
+ if (obj === Math) {
1031
+ if (!ALLOWED_MATH_PROPS.has(methodName)) {
1032
+ throw new Error(`Math.${methodName} is not allowed`);
1033
+ }
1034
+ return;
1035
+ }
1036
+
1037
+ if (obj === JSON) {
1038
+ if (!ALLOWED_JSON_METHODS.has(methodName)) {
1039
+ throw new Error(`JSON.${methodName} is not allowed`);
1040
+ }
1041
+ return;
1042
+ }
1043
+
1044
+ if (obj === Object) {
1045
+ if (!ALLOWED_OBJECT_STATIC.has(methodName)) {
1046
+ throw new Error(`Object.${methodName} is not allowed`);
1047
+ }
1048
+ return;
1049
+ }
1050
+
1051
+ if (obj === Array) {
1052
+ if (!ALLOWED_ARRAY_METHODS.has(methodName) && methodName !== 'from' && methodName !== 'isArray' && methodName !== 'of') {
1053
+ throw new Error(`Array.${methodName} is not allowed`);
1054
+ }
1055
+ return;
1056
+ }
1057
+
1058
+ // For plain objects, allow calling methods that are own properties (user-defined functions)
1059
+ if (typeof obj === 'object' && obj !== null) {
1060
+ if (typeof obj[methodName] === 'function') {
1061
+ // Allow if it's an own property or from a safe prototype
1062
+ return;
1063
+ }
1064
+ }
1065
+
1066
+ throw new Error(`Method call '${methodName}' is not allowed on this object type`);
1067
+ }
1068
+
1069
+ evaluateArgList(args) {
1070
+ const result = [];
1071
+ for (const arg of args) {
1072
+ if (arg.type === 'Spread') {
1073
+ const spreadVal = this.evaluate(arg.argument);
1074
+ if (Array.isArray(spreadVal)) {
1075
+ result.push(...spreadVal);
1076
+ } else {
1077
+ result.push(spreadVal);
1078
+ }
1079
+ } else {
1080
+ result.push(this.evaluate(arg));
1081
+ }
1082
+ }
1083
+ return result;
1084
+ }
1085
+
1086
+ evaluateArrow(node) {
1087
+ const outerContext = this.context;
1088
+
1089
+ return (...args) => {
1090
+ const innerContext = { ...outerContext };
1091
+
1092
+ for (let i = 0; i < node.params.length; i++) {
1093
+ const param = node.params[i];
1094
+ if (typeof param === 'object' && param.type === 'rest') {
1095
+ innerContext[param.name] = args.slice(i);
1096
+ } else {
1097
+ innerContext[param] = args[i];
1098
+ }
1099
+ }
1100
+
1101
+ const innerEval = new Evaluator(innerContext);
1102
+ return innerEval.evaluate(node.body);
1103
+ };
1104
+ }
1105
+
1106
+ evaluateArray(node) {
1107
+ const result = [];
1108
+ for (const el of node.elements) {
1109
+ if (el.type === 'Spread') {
1110
+ const val = this.evaluate(el.argument);
1111
+ if (Array.isArray(val)) {
1112
+ result.push(...val);
1113
+ }
1114
+ } else {
1115
+ result.push(this.evaluate(el));
1116
+ }
1117
+ }
1118
+ return result;
1119
+ }
1120
+
1121
+ evaluateObject(node) {
1122
+ const result = {};
1123
+ for (const prop of node.properties) {
1124
+ if (prop.type === 'SpreadProperty') {
1125
+ const val = this.evaluate(prop.argument);
1126
+ if (typeof val === 'object' && val !== null) {
1127
+ Object.assign(result, val);
1128
+ }
1129
+ } else {
1130
+ const key = prop.computed ? this.evaluate(prop.key) : prop.key;
1131
+ result[key] = this.evaluate(prop.value);
1132
+ }
1133
+ }
1134
+ return result;
1135
+ }
1136
+ }
1137
+
1138
+ // ============================================================
1139
+ // Expression Cache (LRU)
1140
+ // ============================================================
1141
+
1142
+ class ExpressionCache {
1143
+ constructor(maxSize = 500) {
1144
+ this.maxSize = maxSize;
1145
+ this.cache = new Map();
1146
+ }
1147
+
1148
+ get(key) {
1149
+ if (!this.cache.has(key)) return null;
1150
+ // Move to end (most recently used)
1151
+ const value = this.cache.get(key);
1152
+ this.cache.delete(key);
1153
+ this.cache.set(key, value);
1154
+ return value;
1155
+ }
1156
+
1157
+ set(key, value) {
1158
+ if (this.cache.has(key)) {
1159
+ this.cache.delete(key);
1160
+ } else if (this.cache.size >= this.maxSize) {
1161
+ // Delete oldest entry
1162
+ const firstKey = this.cache.keys().next().value;
1163
+ this.cache.delete(firstKey);
1164
+ }
1165
+ this.cache.set(key, value);
1166
+ }
1167
+
1168
+ clear() {
1169
+ this.cache.clear();
1170
+ }
1171
+ }
1172
+
1173
+ // ============================================================
1174
+ // Public API
1175
+ // ============================================================
1176
+
1177
+ class ExpressionEvaluator {
1178
+ constructor() {
1179
+ this.cache = new ExpressionCache(500);
1180
+ }
1181
+
1182
+ /**
1183
+ * Parse and evaluate an expression
1184
+ * @param {string} expression - Expression string
1185
+ * @param {Object} context - Variable context
1186
+ * @returns {*} Result
1187
+ */
1188
+ evaluate(expression, context = {}) {
1189
+ if (!expression || typeof expression !== 'string') return undefined;
1190
+ expression = expression.trim();
1191
+ if (expression === '') return undefined;
1192
+
1193
+ try {
1194
+ // Get or parse AST (cached)
1195
+ let ast = this.cache.get(expression);
1196
+ if (!ast) {
1197
+ const lexer = new Lexer(expression);
1198
+ const parser = new Parser(lexer);
1199
+ ast = parser.parse();
1200
+ this.cache.set(expression, ast);
1201
+ }
1202
+
1203
+ // Evaluate with context
1204
+ const evaluator = new Evaluator(context);
1205
+ return evaluator.evaluate(ast);
1206
+ } catch (error) {
1207
+ // Silent fail in production — return undefined
1208
+ return undefined;
1209
+ }
1210
+ }
1211
+
1212
+ /**
1213
+ * Evaluate and return boolean
1214
+ * @param {string} expression
1215
+ * @param {Object} context
1216
+ * @returns {boolean}
1217
+ */
1218
+ isTruthy(expression, context = {}) {
1219
+ return Boolean(this.evaluate(expression, context));
1220
+ }
1221
+
1222
+ /**
1223
+ * Clear the AST cache
1224
+ */
1225
+ clearCache() {
1226
+ this.cache.clear();
1227
+ }
1228
+ }
1229
+
1230
+ // Singleton instance
1231
+ const expressionEvaluator = new ExpressionEvaluator();
1232
+
1233
+ export { ExpressionEvaluator, ExpressionCache, Lexer, Parser, Evaluator };
1234
+ export default expressionEvaluator;