@enspirit/elo 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/bin/elo +2 -0
  4. package/bin/eloc +2 -0
  5. package/dist/src/ast.d.ts +309 -0
  6. package/dist/src/ast.d.ts.map +1 -0
  7. package/dist/src/ast.js +173 -0
  8. package/dist/src/ast.js.map +1 -0
  9. package/dist/src/bindings/javascript.d.ts +17 -0
  10. package/dist/src/bindings/javascript.d.ts.map +1 -0
  11. package/dist/src/bindings/javascript.js +350 -0
  12. package/dist/src/bindings/javascript.js.map +1 -0
  13. package/dist/src/bindings/ruby.d.ts +20 -0
  14. package/dist/src/bindings/ruby.d.ts.map +1 -0
  15. package/dist/src/bindings/ruby.js +365 -0
  16. package/dist/src/bindings/ruby.js.map +1 -0
  17. package/dist/src/bindings/sql.d.ts +20 -0
  18. package/dist/src/bindings/sql.d.ts.map +1 -0
  19. package/dist/src/bindings/sql.js +319 -0
  20. package/dist/src/bindings/sql.js.map +1 -0
  21. package/dist/src/cli.d.ts +3 -0
  22. package/dist/src/cli.d.ts.map +1 -0
  23. package/dist/src/cli.js +225 -0
  24. package/dist/src/cli.js.map +1 -0
  25. package/dist/src/compile.d.ts +47 -0
  26. package/dist/src/compile.d.ts.map +1 -0
  27. package/dist/src/compile.js +55 -0
  28. package/dist/src/compile.js.map +1 -0
  29. package/dist/src/compilers/javascript.d.ts +41 -0
  30. package/dist/src/compilers/javascript.d.ts.map +1 -0
  31. package/dist/src/compilers/javascript.js +323 -0
  32. package/dist/src/compilers/javascript.js.map +1 -0
  33. package/dist/src/compilers/ruby.d.ts +40 -0
  34. package/dist/src/compilers/ruby.d.ts.map +1 -0
  35. package/dist/src/compilers/ruby.js +326 -0
  36. package/dist/src/compilers/ruby.js.map +1 -0
  37. package/dist/src/compilers/sql.d.ts +37 -0
  38. package/dist/src/compilers/sql.d.ts.map +1 -0
  39. package/dist/src/compilers/sql.js +164 -0
  40. package/dist/src/compilers/sql.js.map +1 -0
  41. package/dist/src/elo.d.ts +3 -0
  42. package/dist/src/elo.d.ts.map +1 -0
  43. package/dist/src/elo.js +187 -0
  44. package/dist/src/elo.js.map +1 -0
  45. package/dist/src/eloc.d.ts +3 -0
  46. package/dist/src/eloc.d.ts.map +1 -0
  47. package/dist/src/eloc.js +232 -0
  48. package/dist/src/eloc.js.map +1 -0
  49. package/dist/src/eval.d.ts +3 -0
  50. package/dist/src/eval.d.ts.map +1 -0
  51. package/dist/src/eval.js +196 -0
  52. package/dist/src/eval.js.map +1 -0
  53. package/dist/src/index.d.ts +17 -0
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +36 -0
  56. package/dist/src/index.js.map +1 -0
  57. package/dist/src/ir.d.ts +295 -0
  58. package/dist/src/ir.d.ts.map +1 -0
  59. package/dist/src/ir.js +224 -0
  60. package/dist/src/ir.js.map +1 -0
  61. package/dist/src/parser.d.ts +137 -0
  62. package/dist/src/parser.d.ts.map +1 -0
  63. package/dist/src/parser.js +1266 -0
  64. package/dist/src/parser.js.map +1 -0
  65. package/dist/src/preludes/index.d.ts +14 -0
  66. package/dist/src/preludes/index.d.ts.map +1 -0
  67. package/dist/src/preludes/index.js +27 -0
  68. package/dist/src/preludes/index.js.map +1 -0
  69. package/dist/src/runtime.d.ts +23 -0
  70. package/dist/src/runtime.d.ts.map +1 -0
  71. package/dist/src/runtime.js +326 -0
  72. package/dist/src/runtime.js.map +1 -0
  73. package/dist/src/stdlib.d.ts +121 -0
  74. package/dist/src/stdlib.d.ts.map +1 -0
  75. package/dist/src/stdlib.js +237 -0
  76. package/dist/src/stdlib.js.map +1 -0
  77. package/dist/src/transform.d.ts +38 -0
  78. package/dist/src/transform.d.ts.map +1 -0
  79. package/dist/src/transform.js +322 -0
  80. package/dist/src/transform.js.map +1 -0
  81. package/dist/src/typedefs.d.ts +50 -0
  82. package/dist/src/typedefs.d.ts.map +1 -0
  83. package/dist/src/typedefs.js +294 -0
  84. package/dist/src/typedefs.js.map +1 -0
  85. package/dist/src/types.d.ts +54 -0
  86. package/dist/src/types.d.ts.map +1 -0
  87. package/dist/src/types.js +62 -0
  88. package/dist/src/types.js.map +1 -0
  89. package/package.json +66 -0
@@ -0,0 +1,1266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Parser = void 0;
4
+ exports.parse = parse;
5
+ const ast_1 = require("./ast");
6
+ class Lexer {
7
+ constructor(input) {
8
+ this.position = 0;
9
+ this.line = 1;
10
+ this.column = 1;
11
+ this.input = input;
12
+ this.current = input[0] || '';
13
+ }
14
+ saveState() {
15
+ return { position: this.position, current: this.current, line: this.line, column: this.column };
16
+ }
17
+ restoreState(state) {
18
+ this.position = state.position;
19
+ this.current = state.current;
20
+ this.line = state.line;
21
+ this.column = state.column;
22
+ }
23
+ advance() {
24
+ // Track line/column before moving
25
+ if (this.current === '\n') {
26
+ this.line++;
27
+ this.column = 1;
28
+ }
29
+ else {
30
+ this.column++;
31
+ }
32
+ this.position++;
33
+ this.current = this.position < this.input.length ? this.input[this.position] : '';
34
+ }
35
+ skipWhitespace() {
36
+ while (this.current) {
37
+ // Skip whitespace
38
+ if (/\s/.test(this.current)) {
39
+ this.advance();
40
+ continue;
41
+ }
42
+ // Skip comments (# to end of line)
43
+ if (this.current === '#') {
44
+ this.skipComment();
45
+ continue;
46
+ }
47
+ break;
48
+ }
49
+ }
50
+ skipComment() {
51
+ // Skip everything until end of line or end of input
52
+ while (this.current && this.current !== '\n') {
53
+ this.advance();
54
+ }
55
+ // Skip the newline itself if present
56
+ if (this.current === '\n') {
57
+ this.advance();
58
+ }
59
+ }
60
+ readNumber() {
61
+ let num = '';
62
+ let hasDot = false;
63
+ while (this.current && /[0-9.]/.test(this.current)) {
64
+ // Stop before consuming '.' if it's part of '..' or '...' range operator
65
+ if (this.current === '.' && this.peek() === '.') {
66
+ break;
67
+ }
68
+ // Only allow one decimal point, and only if followed by a digit
69
+ if (this.current === '.') {
70
+ if (hasDot) {
71
+ // Already have a decimal point, stop here
72
+ break;
73
+ }
74
+ // Check if the next char is a digit - if not, this dot starts a new token
75
+ if (!/[0-9]/.test(this.peek())) {
76
+ break;
77
+ }
78
+ hasDot = true;
79
+ }
80
+ num += this.current;
81
+ this.advance();
82
+ }
83
+ return num;
84
+ }
85
+ readIdentifier() {
86
+ let id = '';
87
+ while (this.current && /[a-zA-Z_0-9]/.test(this.current)) {
88
+ id += this.current;
89
+ this.advance();
90
+ }
91
+ return id;
92
+ }
93
+ peek() {
94
+ return this.position + 1 < this.input.length ? this.input[this.position + 1] : '';
95
+ }
96
+ readStringContent() {
97
+ let str = '';
98
+ while (this.current && this.current !== '"') {
99
+ str += this.current;
100
+ this.advance();
101
+ }
102
+ return str;
103
+ }
104
+ readSingleQuotedString() {
105
+ let str = '';
106
+ while (this.current && this.current !== "'") {
107
+ // Handle escape sequences
108
+ if (this.current === '\\' && this.peek() === "'") {
109
+ this.advance(); // skip backslash
110
+ str += "'";
111
+ this.advance();
112
+ }
113
+ else if (this.current === '\\' && this.peek() === '\\') {
114
+ this.advance(); // skip first backslash
115
+ str += '\\';
116
+ this.advance();
117
+ }
118
+ else {
119
+ str += this.current;
120
+ this.advance();
121
+ }
122
+ }
123
+ return str;
124
+ }
125
+ readDateOrDateTime() {
126
+ // Read ISO8601 date or datetime: 2024-01-15 or 2024-01-15T10:30:00.123Z
127
+ let dateStr = '';
128
+ // Read YYYY-MM-DD part
129
+ while (this.current && /[0-9\-]/.test(this.current)) {
130
+ dateStr += this.current;
131
+ this.advance();
132
+ }
133
+ // Check if there's a time part (T)
134
+ if (this.current === 'T') {
135
+ dateStr += this.current;
136
+ this.advance();
137
+ // Read time part: HH:MM:SS
138
+ while (this.current && /[0-9:]/.test(this.current)) {
139
+ dateStr += this.current;
140
+ this.advance();
141
+ }
142
+ // Optional fractional seconds (.123)
143
+ const frac = this.current;
144
+ if (frac === '.') {
145
+ dateStr += frac;
146
+ this.advance();
147
+ while (this.current && /[0-9]/.test(this.current)) {
148
+ dateStr += this.current;
149
+ this.advance();
150
+ }
151
+ }
152
+ // Optional timezone (Z or +/-HH:MM)
153
+ const cur = this.current;
154
+ if (cur === 'Z') {
155
+ dateStr += cur;
156
+ this.advance();
157
+ }
158
+ else if (cur === '+' || cur === '-') {
159
+ dateStr += cur;
160
+ this.advance();
161
+ while (this.current && /[0-9:]/.test(this.current)) {
162
+ dateStr += this.current;
163
+ this.advance();
164
+ }
165
+ }
166
+ }
167
+ return dateStr;
168
+ }
169
+ readDuration() {
170
+ // Read ISO8601 duration: P1D, PT1H30M, P1Y2M3DT4H5M6S, P2W, etc.
171
+ // Date components (Y, M, W, D) come before T
172
+ // Time components (H, M, S) must come after T
173
+ let duration = '';
174
+ duration += this.current; // P
175
+ this.advance();
176
+ let seenT = false;
177
+ // Read date part (Y, M, W, D) and/or time part (T followed by H, M, S)
178
+ while (this.current && /[0-9YMWDTHMS.]/.test(this.current)) {
179
+ const char = this.current;
180
+ // Track when we see T (time separator)
181
+ if (char === 'T') {
182
+ seenT = true;
183
+ }
184
+ // H and S are only valid after T (time components)
185
+ // M after T means minutes, M before T means months
186
+ if ((char === 'H' || char === 'S') && !seenT) {
187
+ // Invalid: H or S without T prefix - stop reading duration here
188
+ break;
189
+ }
190
+ duration += char;
191
+ this.advance();
192
+ }
193
+ return duration;
194
+ }
195
+ nextToken() {
196
+ this.skipWhitespace();
197
+ if (!this.current) {
198
+ return { type: 'EOF', value: '', position: this.position, line: this.line, column: this.column };
199
+ }
200
+ const pos = this.position;
201
+ const line = this.line;
202
+ const col = this.column;
203
+ // Numbers
204
+ if (/[0-9]/.test(this.current)) {
205
+ return { type: 'NUMBER', value: this.readNumber(), position: pos, line, column: col };
206
+ }
207
+ // Date/DateTime literals: D2024-01-15 or D2024-01-15T10:30:00Z
208
+ if (this.current === 'D') {
209
+ const nextChar = this.peek();
210
+ if (/[0-9]/.test(nextChar)) {
211
+ // Date or DateTime literal starting with D
212
+ this.advance(); // skip 'D'
213
+ const dateTimeStr = this.readDateOrDateTime();
214
+ // Distinguish between DATE and DATETIME based on presence of 'T'
215
+ if (dateTimeStr.includes('T')) {
216
+ return { type: 'DATETIME', value: dateTimeStr, position: pos, line, column: col };
217
+ }
218
+ else {
219
+ return { type: 'DATE', value: dateTimeStr, position: pos, line, column: col };
220
+ }
221
+ }
222
+ }
223
+ // ISO8601 Duration: P1D, PT1H30M, P1Y2M3D, etc.
224
+ if (this.current === 'P') {
225
+ // Save state in case this isn't actually a duration
226
+ const savedState = this.saveState();
227
+ const durationStr = this.readDuration();
228
+ if (durationStr.length > 1) {
229
+ return { type: 'DURATION', value: durationStr, position: pos, line, column: col };
230
+ }
231
+ // Not a valid duration, restore state and continue to identifier parsing
232
+ this.restoreState(savedState);
233
+ }
234
+ // Single-quoted strings: 'hello world'
235
+ if (this.current === "'") {
236
+ this.advance(); // skip opening quote
237
+ const str = this.readSingleQuotedString();
238
+ this.advance(); // skip closing quote
239
+ return { type: 'STRING', value: str, position: pos, line, column: col };
240
+ }
241
+ // Identifiers and keywords (true, false, let, in, NOW, TODAY, TOMORROW, YESTERDAY)
242
+ if (/[a-zA-Z_]/.test(this.current)) {
243
+ const id = this.readIdentifier();
244
+ if (id === 'true' || id === 'false') {
245
+ return { type: 'BOOLEAN', value: id, position: pos, line, column: col };
246
+ }
247
+ if (id === 'null') {
248
+ return { type: 'NULL', value: id, position: pos, line, column: col };
249
+ }
250
+ if (id === 'let') {
251
+ return { type: 'LET', value: id, position: pos, line, column: col };
252
+ }
253
+ if (id === 'in') {
254
+ return { type: 'IN', value: id, position: pos, line, column: col };
255
+ }
256
+ if (id === 'if') {
257
+ return { type: 'IF', value: id, position: pos, line, column: col };
258
+ }
259
+ if (id === 'then') {
260
+ return { type: 'THEN', value: id, position: pos, line, column: col };
261
+ }
262
+ if (id === 'else') {
263
+ return { type: 'ELSE', value: id, position: pos, line, column: col };
264
+ }
265
+ if (id === 'and') {
266
+ return { type: 'AND', value: id, position: pos, line, column: col };
267
+ }
268
+ if (id === 'or') {
269
+ return { type: 'OR', value: id, position: pos, line, column: col };
270
+ }
271
+ if (id === 'not') {
272
+ return { type: 'NOT', value: id, position: pos, line, column: col };
273
+ }
274
+ if (id === 'fn') {
275
+ return { type: 'FN', value: id, position: pos, line, column: col };
276
+ }
277
+ // Uppercase identifiers: types, selectors, temporal keywords
278
+ if (/^[A-Z]/.test(id)) {
279
+ return { type: 'UPPER_IDENTIFIER', value: id, position: pos, line, column: col };
280
+ }
281
+ // Lowercase identifiers: user variables
282
+ return { type: 'IDENTIFIER', value: id, position: pos, line, column: col };
283
+ }
284
+ // Multi-character operators
285
+ const char = this.current;
286
+ const next = this.peek();
287
+ // Two-character operators
288
+ if (char === '<' && next === '=') {
289
+ this.advance();
290
+ this.advance();
291
+ return { type: 'LTE', value: '<=', position: pos, line, column: col };
292
+ }
293
+ if (char === '>' && next === '=') {
294
+ this.advance();
295
+ this.advance();
296
+ return { type: 'GTE', value: '>=', position: pos, line, column: col };
297
+ }
298
+ if (char === '=') {
299
+ if (next === '=') {
300
+ this.advance();
301
+ this.advance();
302
+ return { type: 'EQ', value: '==', position: pos, line, column: col };
303
+ }
304
+ // Single = is ASSIGN
305
+ this.advance();
306
+ return { type: 'ASSIGN', value: '=', position: pos, line, column: col };
307
+ }
308
+ if (char === '!' && next === '=') {
309
+ this.advance();
310
+ this.advance();
311
+ return { type: 'NEQ', value: '!=', position: pos, line, column: col };
312
+ }
313
+ if (char === '&' && next === '&') {
314
+ this.advance();
315
+ this.advance();
316
+ return { type: 'AND', value: '&&', position: pos, line, column: col };
317
+ }
318
+ if (char === '|') {
319
+ if (next === '>') {
320
+ // Pipe operator for chaining: a |> f(b)
321
+ this.advance();
322
+ this.advance();
323
+ return { type: 'PIPE_OP', value: '|>', position: pos, line, column: col };
324
+ }
325
+ if (next === '|') {
326
+ this.advance();
327
+ this.advance();
328
+ return { type: 'OR', value: '||', position: pos, line, column: col };
329
+ }
330
+ // Single pipe for predicate syntax: fn( x | body )
331
+ this.advance();
332
+ return { type: 'PIPE', value: '|', position: pos, line, column: col };
333
+ }
334
+ // Arrow for lambda syntax: fn( x ~> body )
335
+ if (char === '~' && next === '>') {
336
+ this.advance();
337
+ this.advance();
338
+ return { type: 'ARROW', value: '~>', position: pos, line, column: col };
339
+ }
340
+ // Range operators: .. (inclusive) and ... (exclusive)
341
+ if (char === '.' && next === '.') {
342
+ this.advance();
343
+ this.advance();
344
+ if (this.current === '.') {
345
+ this.advance();
346
+ return { type: 'RANGE_EXCL', value: '...', position: pos, line, column: col };
347
+ }
348
+ return { type: 'RANGE_INCL', value: '..', position: pos, line, column: col };
349
+ }
350
+ // Single-character operators
351
+ this.advance();
352
+ switch (char) {
353
+ case '+': return { type: 'PLUS', value: char, position: pos, line, column: col };
354
+ case '-': return { type: 'MINUS', value: char, position: pos, line, column: col };
355
+ case '*': return { type: 'STAR', value: char, position: pos, line, column: col };
356
+ case '/': return { type: 'SLASH', value: char, position: pos, line, column: col };
357
+ case '%': return { type: 'PERCENT', value: char, position: pos, line, column: col };
358
+ case '^': return { type: 'CARET', value: char, position: pos, line, column: col };
359
+ case '(': return { type: 'LPAREN', value: char, position: pos, line, column: col };
360
+ case ')': return { type: 'RPAREN', value: char, position: pos, line, column: col };
361
+ case '{': return { type: 'LBRACE', value: char, position: pos, line, column: col };
362
+ case '}': return { type: 'RBRACE', value: char, position: pos, line, column: col };
363
+ case '[': return { type: 'LBRACKET', value: char, position: pos, line, column: col };
364
+ case ']': return { type: 'RBRACKET', value: char, position: pos, line, column: col };
365
+ case ',': return { type: 'COMMA', value: char, position: pos, line, column: col };
366
+ case ':': return { type: 'COLON', value: char, position: pos, line, column: col };
367
+ case '?': return { type: 'QUESTION', value: char, position: pos, line, column: col };
368
+ case '.':
369
+ return { type: 'DOT', value: char, position: pos, line, column: col };
370
+ case '<': return { type: 'LT', value: char, position: pos, line, column: col };
371
+ case '>': return { type: 'GT', value: char, position: pos, line, column: col };
372
+ case '!': return { type: 'NOT', value: char, position: pos, line, column: col };
373
+ default:
374
+ throw new Error(`Unexpected character '${char}' at line ${line}, column ${col}`);
375
+ }
376
+ }
377
+ }
378
+ const DEFAULT_MAX_DEPTH = 100;
379
+ class Parser {
380
+ constructor(input, options = {}) {
381
+ this.depth = 0;
382
+ this.lexer = new Lexer(input);
383
+ this.currentToken = this.lexer.nextToken();
384
+ this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
385
+ }
386
+ checkDepth() {
387
+ if (this.depth > this.maxDepth) {
388
+ throw new Error(`Maximum expression depth exceeded (${this.maxDepth})`);
389
+ }
390
+ }
391
+ saveState() {
392
+ return {
393
+ lexerState: this.lexer.saveState(),
394
+ currentToken: { ...this.currentToken },
395
+ depth: this.depth
396
+ };
397
+ }
398
+ restoreState(state) {
399
+ this.lexer.restoreState(state.lexerState);
400
+ this.currentToken = state.currentToken;
401
+ this.depth = state.depth;
402
+ }
403
+ formatLocation(token) {
404
+ return `line ${token.line}, column ${token.column}`;
405
+ }
406
+ eat(tokenType) {
407
+ if (this.currentToken.type === tokenType) {
408
+ this.currentToken = this.lexer.nextToken();
409
+ }
410
+ else {
411
+ throw new Error(`Expected ${tokenType} but got ${this.currentToken.type} at ${this.formatLocation(this.currentToken)}`);
412
+ }
413
+ }
414
+ primary() {
415
+ const token = this.currentToken;
416
+ if (token.type === 'NUMBER') {
417
+ this.eat('NUMBER');
418
+ return (0, ast_1.literal)(parseFloat(token.value));
419
+ }
420
+ if (token.type === 'BOOLEAN') {
421
+ this.eat('BOOLEAN');
422
+ return (0, ast_1.literal)(token.value === 'true');
423
+ }
424
+ if (token.type === 'NULL') {
425
+ this.eat('NULL');
426
+ return (0, ast_1.nullLiteral)();
427
+ }
428
+ if (token.type === 'DATE') {
429
+ this.eat('DATE');
430
+ return (0, ast_1.dateLiteral)(token.value);
431
+ }
432
+ if (token.type === 'DATETIME') {
433
+ this.eat('DATETIME');
434
+ return (0, ast_1.dateTimeLiteral)(token.value);
435
+ }
436
+ if (token.type === 'DURATION') {
437
+ this.eat('DURATION');
438
+ return (0, ast_1.durationLiteral)(token.value);
439
+ }
440
+ if (token.type === 'STRING') {
441
+ this.eat('STRING');
442
+ return (0, ast_1.stringLiteral)(token.value);
443
+ }
444
+ // Uppercase identifiers: temporal keywords, types, selectors
445
+ if (token.type === 'UPPER_IDENTIFIER') {
446
+ const name = token.value;
447
+ this.eat('UPPER_IDENTIFIER');
448
+ // Check if this is a temporal keyword
449
+ const temporalKeywords = ['NOW', 'TODAY', 'TOMORROW', 'YESTERDAY',
450
+ 'SOD', 'EOD', 'SOW', 'EOW', 'SOM', 'EOM', 'SOQ', 'EOQ', 'SOY', 'EOY',
451
+ 'BOT', 'EOT'];
452
+ if (temporalKeywords.includes(name)) {
453
+ return (0, ast_1.temporalKeyword)(name);
454
+ }
455
+ // Check if this is a function call (uppercase functions like Type selectors)
456
+ if (this.currentToken.type === 'LPAREN') {
457
+ this.eat('LPAREN');
458
+ const args = [];
459
+ const tok = this.currentToken;
460
+ if (tok.type !== 'RPAREN') {
461
+ args.push(this.expr());
462
+ while (this.currentToken.type === 'COMMA') {
463
+ this.eat('COMMA');
464
+ args.push(this.expr());
465
+ }
466
+ }
467
+ this.eat('RPAREN');
468
+ return (0, ast_1.functionCall)(name, args);
469
+ }
470
+ throw new Error(`Unknown uppercase identifier '${name}' at ${this.formatLocation(token)}`);
471
+ }
472
+ // Lowercase identifiers: user variables, function calls
473
+ if (token.type === 'IDENTIFIER') {
474
+ const name = token.value;
475
+ this.eat('IDENTIFIER');
476
+ // Lambda sugar: x ~> body (single parameter only)
477
+ // Body parsed at pipe() level to stop before == and other low-precedence operators
478
+ // Use fn(x ~> body) syntax for bodies containing == comparisons
479
+ if (this.currentToken.type === 'ARROW') {
480
+ this.eat('ARROW');
481
+ const body = this.pipe();
482
+ return (0, ast_1.lambda)([name], body);
483
+ }
484
+ // Check if this is a function call
485
+ if (this.currentToken.type === 'LPAREN') {
486
+ this.eat('LPAREN');
487
+ const args = [];
488
+ // Parse arguments
489
+ // After eat(), currentToken changes - use type assertion to tell TypeScript
490
+ const tok = this.currentToken;
491
+ if (tok.type !== 'RPAREN') {
492
+ args.push(this.expr());
493
+ while (this.currentToken.type === 'COMMA') {
494
+ this.eat('COMMA');
495
+ args.push(this.expr());
496
+ }
497
+ }
498
+ this.eat('RPAREN');
499
+ return (0, ast_1.functionCall)(name, args);
500
+ }
501
+ // Otherwise, it's a variable
502
+ return (0, ast_1.variable)(name);
503
+ }
504
+ if (token.type === 'LPAREN') {
505
+ this.eat('LPAREN');
506
+ const expr = this.expr();
507
+ this.eat('RPAREN');
508
+ return expr;
509
+ }
510
+ // Handle let expressions (can appear anywhere an expression is expected)
511
+ if (token.type === 'LET') {
512
+ return this.letExpr();
513
+ }
514
+ // Handle if expressions (can appear anywhere an expression is expected)
515
+ if (token.type === 'IF') {
516
+ return this.ifExprParse();
517
+ }
518
+ // Handle lambda expressions: fn( x | body ) or fn( x, y | body )
519
+ if (token.type === 'FN') {
520
+ return this.lambdaParse();
521
+ }
522
+ // Handle object literals: {key: value, ...}
523
+ if (token.type === 'LBRACE') {
524
+ return this.objectParse();
525
+ }
526
+ // Handle array literals: [expr, expr, ...]
527
+ if (token.type === 'LBRACKET') {
528
+ return this.arrayParse();
529
+ }
530
+ // Handle datapath literals: .x.y.z
531
+ if (token.type === 'DOT') {
532
+ return this.datapathParse();
533
+ }
534
+ throw new Error(`Unexpected token ${token.type} at ${this.formatLocation(token)}`);
535
+ }
536
+ postfix() {
537
+ let expr = this.primary();
538
+ // Handle member access (dot notation) and function application
539
+ while (this.currentToken.type === 'DOT' || this.currentToken.type === 'LPAREN') {
540
+ if (this.currentToken.type === 'DOT') {
541
+ this.eat('DOT');
542
+ const property = this.currentToken.value;
543
+ this.eat('IDENTIFIER');
544
+ expr = (0, ast_1.memberAccess)(expr, property);
545
+ }
546
+ else {
547
+ // Function application: expr(args)
548
+ this.eat('LPAREN');
549
+ const args = [];
550
+ // After eat(), currentToken changes - use type assertion to tell TypeScript
551
+ if (this.currentToken.type !== 'RPAREN') {
552
+ args.push(this.expr());
553
+ while (this.currentToken.type === 'COMMA') {
554
+ this.eat('COMMA');
555
+ args.push(this.expr());
556
+ }
557
+ }
558
+ this.eat('RPAREN');
559
+ expr = (0, ast_1.apply)(expr, args);
560
+ }
561
+ }
562
+ return expr;
563
+ }
564
+ unary() {
565
+ if (this.currentToken.type === 'NOT') {
566
+ this.eat('NOT');
567
+ return (0, ast_1.unary)('!', this.unary());
568
+ }
569
+ if (this.currentToken.type === 'PLUS') {
570
+ this.eat('PLUS');
571
+ return (0, ast_1.unary)('+', this.unary());
572
+ }
573
+ if (this.currentToken.type === 'MINUS') {
574
+ this.eat('MINUS');
575
+ return (0, ast_1.unary)('-', this.unary());
576
+ }
577
+ return this.postfix();
578
+ }
579
+ power() {
580
+ let node = this.unary();
581
+ // Right-associative
582
+ if (this.currentToken.type === 'CARET') {
583
+ this.eat('CARET');
584
+ node = (0, ast_1.binary)('^', node, this.power());
585
+ }
586
+ return node;
587
+ }
588
+ factor() {
589
+ let node = this.power();
590
+ while (['STAR', 'SLASH', 'PERCENT'].includes(this.currentToken.type)) {
591
+ const token = this.currentToken;
592
+ if (token.type === 'STAR') {
593
+ this.eat('STAR');
594
+ node = (0, ast_1.binary)('*', node, this.power());
595
+ }
596
+ else if (token.type === 'SLASH') {
597
+ this.eat('SLASH');
598
+ node = (0, ast_1.binary)('/', node, this.power());
599
+ }
600
+ else if (token.type === 'PERCENT') {
601
+ this.eat('PERCENT');
602
+ node = (0, ast_1.binary)('%', node, this.power());
603
+ }
604
+ }
605
+ return node;
606
+ }
607
+ term() {
608
+ return this.factor();
609
+ }
610
+ addition() {
611
+ let node = this.term();
612
+ while (['PLUS', 'MINUS'].includes(this.currentToken.type)) {
613
+ const token = this.currentToken;
614
+ if (token.type === 'PLUS') {
615
+ this.eat('PLUS');
616
+ node = (0, ast_1.binary)('+', node, this.term());
617
+ }
618
+ else if (token.type === 'MINUS') {
619
+ this.eat('MINUS');
620
+ node = (0, ast_1.binary)('-', node, this.term());
621
+ }
622
+ }
623
+ return node;
624
+ }
625
+ comparison() {
626
+ let node = this.addition();
627
+ // Handle range membership: expr in expr..expr or expr in expr...expr
628
+ // Also handles: expr not in expr..expr (negated range membership)
629
+ // Use speculative parsing - if no range operator found after IN, restore state
630
+ if (this.currentToken.type === 'IN') {
631
+ const result = this.tryParseRangeMembership(node, false);
632
+ if (result !== null) {
633
+ return result;
634
+ }
635
+ // Not a range expression, continue with normal parsing
636
+ }
637
+ // Handle "not in" for negated range membership
638
+ if (this.currentToken.type === 'NOT') {
639
+ const result = this.tryParseRangeMembership(node, true);
640
+ if (result !== null) {
641
+ return result;
642
+ }
643
+ // Not a range expression, continue with normal parsing
644
+ }
645
+ while (['LT', 'GT', 'LTE', 'GTE'].includes(this.currentToken.type)) {
646
+ const token = this.currentToken;
647
+ if (token.type === 'LT') {
648
+ this.eat('LT');
649
+ node = (0, ast_1.binary)('<', node, this.addition());
650
+ }
651
+ else if (token.type === 'GT') {
652
+ this.eat('GT');
653
+ node = (0, ast_1.binary)('>', node, this.addition());
654
+ }
655
+ else if (token.type === 'LTE') {
656
+ this.eat('LTE');
657
+ node = (0, ast_1.binary)('<=', node, this.addition());
658
+ }
659
+ else if (token.type === 'GTE') {
660
+ this.eat('GTE');
661
+ node = (0, ast_1.binary)('>=', node, this.addition());
662
+ }
663
+ }
664
+ return node;
665
+ }
666
+ equality() {
667
+ let node = this.comparison();
668
+ while (['EQ', 'NEQ'].includes(this.currentToken.type)) {
669
+ const token = this.currentToken;
670
+ if (token.type === 'EQ') {
671
+ this.eat('EQ');
672
+ node = (0, ast_1.binary)('==', node, this.comparison());
673
+ }
674
+ else if (token.type === 'NEQ') {
675
+ this.eat('NEQ');
676
+ node = (0, ast_1.binary)('!=', node, this.comparison());
677
+ }
678
+ }
679
+ return node;
680
+ }
681
+ logical_and() {
682
+ let node = this.alternative();
683
+ while (this.currentToken.type === 'AND') {
684
+ this.eat('AND');
685
+ node = (0, ast_1.binary)('&&', node, this.alternative());
686
+ }
687
+ return node;
688
+ }
689
+ /**
690
+ * Parse alternative expressions: a | b | c
691
+ * Returns first non-null value, left-to-right evaluation.
692
+ */
693
+ alternative() {
694
+ let node = this.equality();
695
+ const alternatives = [node];
696
+ while (this.currentToken.type === 'PIPE') {
697
+ this.eat('PIPE');
698
+ alternatives.push(this.equality());
699
+ }
700
+ if (alternatives.length === 1) {
701
+ return node;
702
+ }
703
+ return (0, ast_1.alternative)(alternatives);
704
+ }
705
+ logical_or() {
706
+ let node = this.logical_and();
707
+ while (this.currentToken.type === 'OR') {
708
+ this.eat('OR');
709
+ node = (0, ast_1.binary)('||', node, this.logical_and());
710
+ }
711
+ return node;
712
+ }
713
+ /**
714
+ * Parse pipe expressions: a |> f(b) |> g(c) or a |> f |> g
715
+ * Desugars to: g(f(a, b), c) or g(f(a))
716
+ * Left-associative, lowest precedence (below logical_or)
717
+ * Parentheses are optional: a |> f is equivalent to a |> f()
718
+ */
719
+ pipe() {
720
+ let node = this.logical_or();
721
+ while (this.currentToken.type === 'PIPE_OP') {
722
+ this.eat('PIPE_OP');
723
+ // Right side must be an identifier (function name) or uppercase (type name)
724
+ const tok = this.currentToken;
725
+ if (tok.type !== 'IDENTIFIER' && tok.type !== 'UPPER_IDENTIFIER') {
726
+ throw new Error(`Expected function name after |> at ${this.formatLocation(tok)}`);
727
+ }
728
+ const funcName = tok.value;
729
+ this.eat(tok.type);
730
+ const args = [node]; // Left side becomes first argument
731
+ // Parentheses are optional - if present, parse additional arguments
732
+ const tok2 = this.currentToken;
733
+ if (tok2.type === 'LPAREN') {
734
+ this.eat('LPAREN');
735
+ // Parse additional arguments
736
+ if (this.currentToken.type !== 'RPAREN') {
737
+ args.push(this.expr());
738
+ while (this.currentToken.type === 'COMMA') {
739
+ this.eat('COMMA');
740
+ args.push(this.expr());
741
+ }
742
+ }
743
+ this.eat('RPAREN');
744
+ }
745
+ node = (0, ast_1.functionCall)(funcName, args);
746
+ }
747
+ return node;
748
+ }
749
+ /**
750
+ * Try to parse range membership: expr in expr..expr or expr in expr...expr
751
+ * Also handles: expr not in expr..expr (when negated is true)
752
+ * Returns null if this is not a range expression (e.g., it's 'in' from 'let...in')
753
+ */
754
+ tryParseRangeMembership(node, negated) {
755
+ const savedState = this.saveState();
756
+ // For "not in", consume NOT first, then expect IN
757
+ if (negated) {
758
+ this.eat('NOT');
759
+ if (this.currentToken.type !== 'IN') {
760
+ this.restoreState(savedState);
761
+ return null;
762
+ }
763
+ }
764
+ this.eat('IN');
765
+ const rangeStart = this.addition();
766
+ let inclusive;
767
+ if (this.currentToken.type === 'RANGE_INCL') {
768
+ this.eat('RANGE_INCL');
769
+ inclusive = true;
770
+ }
771
+ else if (this.currentToken.type === 'RANGE_EXCL') {
772
+ this.eat('RANGE_EXCL');
773
+ inclusive = false;
774
+ }
775
+ else {
776
+ // No range operator found - this is not a range expression
777
+ // Restore state and return null
778
+ this.restoreState(savedState);
779
+ return null;
780
+ }
781
+ const rangeEnd = this.addition();
782
+ const rangeExpr = this.desugarRangeMembership(node, rangeStart, rangeEnd, inclusive);
783
+ // Wrap in NOT if negated
784
+ if (negated) {
785
+ return (0, ast_1.unary)('!', rangeExpr);
786
+ }
787
+ return rangeExpr;
788
+ }
789
+ /**
790
+ * Desugar range membership expression.
791
+ * `value in start..end` becomes `value >= start and value <= end`
792
+ * `value in start...end` becomes `value >= start and value < end`
793
+ *
794
+ * For complex expressions, wraps in let to avoid multiple evaluation.
795
+ */
796
+ desugarRangeMembership(value, start, end, inclusive) {
797
+ const isSimple = (e) => {
798
+ return e.type === 'literal' || e.type === 'variable' ||
799
+ e.type === 'date' || e.type === 'datetime' ||
800
+ e.type === 'temporal_keyword';
801
+ };
802
+ const endOp = inclusive ? '<=' : '<';
803
+ if (isSimple(value) && isSimple(start) && isSimple(end)) {
804
+ // Direct expansion for simple expressions
805
+ return (0, ast_1.binary)('&&', (0, ast_1.binary)('>=', value, start), (0, ast_1.binary)(endOp, value, end));
806
+ }
807
+ // Wrap in let to avoid multiple evaluation
808
+ return (0, ast_1.letExpr)([
809
+ { name: '_v', value },
810
+ { name: '_lo', value: start },
811
+ { name: '_hi', value: end }
812
+ ], (0, ast_1.binary)('&&', (0, ast_1.binary)('>=', (0, ast_1.variable)('_v'), (0, ast_1.variable)('_lo')), (0, ast_1.binary)(endOp, (0, ast_1.variable)('_v'), (0, ast_1.variable)('_hi'))));
813
+ }
814
+ letExpr() {
815
+ this.eat('LET');
816
+ // Check if this is a type definition (uppercase identifier)
817
+ if (this.currentToken.type === 'UPPER_IDENTIFIER') {
818
+ return this.typeDefExpr();
819
+ }
820
+ const bindings = [];
821
+ // Parse first binding
822
+ const firstName = this.currentToken.value;
823
+ this.eat('IDENTIFIER');
824
+ this.eat('ASSIGN');
825
+ // Binding values parsed at logical_or level (prevents unparenthesized nested let in bindings)
826
+ const firstValue = this.logical_or();
827
+ bindings.push({ name: firstName, value: firstValue });
828
+ // Parse additional bindings
829
+ while (this.currentToken.type === 'COMMA') {
830
+ this.eat('COMMA');
831
+ const name = this.currentToken.value;
832
+ this.eat('IDENTIFIER');
833
+ this.eat('ASSIGN');
834
+ const value = this.logical_or();
835
+ bindings.push({ name, value });
836
+ }
837
+ this.eat('IN');
838
+ const body = this.expr(); // Body can be any expression including nested let
839
+ return (0, ast_1.letExpr)(bindings, body);
840
+ }
841
+ /**
842
+ * Parse a type definition: let Person = { name: String, age: Int } in body
843
+ * Called after 'let' when we see an UPPER_IDENTIFIER
844
+ * Supports multiple bindings: let Person = {...}, Persons = [Person] in body
845
+ */
846
+ typeDefExpr() {
847
+ const typeName = this.currentToken.value;
848
+ this.eat('UPPER_IDENTIFIER');
849
+ this.eat('ASSIGN');
850
+ const typeExpr = this.typeExpr();
851
+ // Check for additional bindings
852
+ if (this.currentToken.type === 'COMMA') {
853
+ this.eat('COMMA');
854
+ // Next binding could be another type def or a value binding
855
+ // Use type assertion since eat() updates currentToken but TS doesn't track this
856
+ const nextTokenType = this.currentToken.type;
857
+ if (nextTokenType === 'UPPER_IDENTIFIER') {
858
+ // Another type definition - recurse
859
+ const body = this.typeDefExpr();
860
+ return (0, ast_1.typeDef)(typeName, typeExpr, body);
861
+ }
862
+ else {
863
+ // Value binding - parse remaining as let bindings
864
+ const body = this.parseLetBindingsAfterComma();
865
+ return (0, ast_1.typeDef)(typeName, typeExpr, body);
866
+ }
867
+ }
868
+ this.eat('IN');
869
+ const body = this.expr();
870
+ return (0, ast_1.typeDef)(typeName, typeExpr, body);
871
+ }
872
+ /**
873
+ * Parse remaining let bindings after a comma (when mixing type and value bindings)
874
+ * Returns a LetExpr with the remaining bindings
875
+ */
876
+ parseLetBindingsAfterComma() {
877
+ const bindings = [];
878
+ // Parse first binding after comma
879
+ const firstName = this.currentToken.value;
880
+ this.eat('IDENTIFIER');
881
+ this.eat('ASSIGN');
882
+ const firstValue = this.logical_or();
883
+ bindings.push({ name: firstName, value: firstValue });
884
+ // Parse additional bindings
885
+ while (this.currentToken.type === 'COMMA') {
886
+ this.eat('COMMA');
887
+ const name = this.currentToken.value;
888
+ this.eat('IDENTIFIER');
889
+ this.eat('ASSIGN');
890
+ const value = this.logical_or();
891
+ bindings.push({ name, value });
892
+ }
893
+ this.eat('IN');
894
+ const body = this.expr();
895
+ return (0, ast_1.letExpr)(bindings, body);
896
+ }
897
+ /**
898
+ * Parse a type expression: String, Int|String, Int(i | i > 0), [Int], { prop: TypeExpr, ... }
899
+ * Handles union types: Int|String|Bool
900
+ */
901
+ typeExpr() {
902
+ const first = this.typeExprPrimary();
903
+ // Check for union type: Type|Type|...
904
+ if (this.currentToken.type === 'PIPE') {
905
+ const types = [first];
906
+ while (this.currentToken.type === 'PIPE') {
907
+ this.eat('PIPE');
908
+ types.push(this.typeExprPrimary());
909
+ }
910
+ return (0, ast_1.unionType)(types);
911
+ }
912
+ return first;
913
+ }
914
+ /**
915
+ * Parse a primary type expression (without union)
916
+ */
917
+ typeExprPrimary() {
918
+ // Check for '.' (Any type shorthand)
919
+ if (this.currentToken.type === 'DOT') {
920
+ this.eat('DOT');
921
+ return (0, ast_1.typeRef)('Any');
922
+ }
923
+ // Check for array type: [TypeExpr]
924
+ if (this.currentToken.type === 'LBRACKET') {
925
+ this.eat('LBRACKET');
926
+ const elementType = this.typeExpr();
927
+ this.eat('RBRACKET');
928
+ return (0, ast_1.arrayType)(elementType);
929
+ }
930
+ // Check for object schema: { prop: Type, ... }
931
+ if (this.currentToken.type === 'LBRACE') {
932
+ return this.typeSchemaExpr();
933
+ }
934
+ // Must be a type name (UPPER_IDENTIFIER)
935
+ if (this.currentToken.type !== 'UPPER_IDENTIFIER') {
936
+ throw new Error(`Expected type name, '[', or '{' at ${this.formatLocation(this.currentToken)}, got ${this.currentToken.type}`);
937
+ }
938
+ const name = this.currentToken.value;
939
+ this.eat('UPPER_IDENTIFIER');
940
+ const baseType = (0, ast_1.typeRef)(name);
941
+ // Check for subtype constraint: Int(i | i > 0)
942
+ if (this.currentToken.type === 'LPAREN') {
943
+ return this.subtypeConstraintExpr(baseType);
944
+ }
945
+ return baseType;
946
+ }
947
+ /**
948
+ * Parse a subtype constraint: Int(i | i > 0)
949
+ * Called after the base type has been parsed, when we see '('
950
+ */
951
+ subtypeConstraintExpr(baseType) {
952
+ this.eat('LPAREN');
953
+ // Parse variable name
954
+ const varName = this.currentToken.value;
955
+ this.eat('IDENTIFIER');
956
+ // Expect '|' separator
957
+ this.eat('PIPE');
958
+ // Parse constraint expression
959
+ const constraint = this.expr();
960
+ this.eat('RPAREN');
961
+ return (0, ast_1.subtypeConstraint)(baseType, varName, constraint);
962
+ }
963
+ /**
964
+ * Parse a type schema: { name: String, age: Int, nickname :? String, ... } or { name: String, ...: Int }
965
+ * extras:
966
+ * - { x: Int } - closed, no extra attrs allowed
967
+ * - { x: Int, ... } - ignored, extra attrs allowed but not included
968
+ * - { x: Int, ...: String } - typed, extra attrs must match type
969
+ */
970
+ typeSchemaExpr() {
971
+ this.eat('LBRACE');
972
+ const properties = [];
973
+ let extras = undefined;
974
+ // Handle empty schema
975
+ if (this.currentToken.type === 'RBRACE') {
976
+ this.eat('RBRACE');
977
+ return (0, ast_1.typeSchema)(properties, extras);
978
+ }
979
+ // Handle spread only: { ... } or { ...: Type }
980
+ if (this.currentToken.type === 'RANGE_EXCL') {
981
+ this.eat('RANGE_EXCL');
982
+ if (this.currentToken.type === 'COLON') {
983
+ this.eat('COLON');
984
+ extras = this.typeExpr();
985
+ }
986
+ else {
987
+ extras = 'ignored';
988
+ }
989
+ this.eat('RBRACE');
990
+ return (0, ast_1.typeSchema)(properties, extras);
991
+ }
992
+ // Parse first property
993
+ const firstName = this.currentToken.value;
994
+ this.eat('IDENTIFIER');
995
+ this.eat('COLON');
996
+ const firstOptional = this.currentToken.type === 'QUESTION';
997
+ if (firstOptional)
998
+ this.eat('QUESTION');
999
+ const firstType = this.typeExpr();
1000
+ properties.push({ key: firstName, typeExpr: firstType, optional: firstOptional || undefined });
1001
+ // Parse additional properties (commas are optional, like Finitio)
1002
+ while (true) {
1003
+ // Consume optional comma
1004
+ if (this.currentToken.type === 'COMMA') {
1005
+ this.eat('COMMA');
1006
+ }
1007
+ // Check for spread operator: ... or ...: Type
1008
+ if (this.currentToken.type === 'RANGE_EXCL') {
1009
+ this.eat('RANGE_EXCL');
1010
+ if (this.currentToken.type === 'COLON') {
1011
+ this.eat('COLON');
1012
+ extras = this.typeExpr();
1013
+ }
1014
+ else {
1015
+ extras = 'ignored';
1016
+ }
1017
+ break; // spread must be last
1018
+ }
1019
+ // Check for another property (identifier starts next property)
1020
+ if (this.currentToken.type !== 'IDENTIFIER') {
1021
+ break; // no more properties
1022
+ }
1023
+ const name = this.currentToken.value;
1024
+ this.eat('IDENTIFIER');
1025
+ this.eat('COLON');
1026
+ const optional = this.currentToken.type === 'QUESTION';
1027
+ if (optional)
1028
+ this.eat('QUESTION');
1029
+ const propType = this.typeExpr();
1030
+ properties.push({ key: name, typeExpr: propType, optional: optional || undefined });
1031
+ }
1032
+ this.eat('RBRACE');
1033
+ return (0, ast_1.typeSchema)(properties, extras);
1034
+ }
1035
+ ifExprParse() {
1036
+ this.eat('IF');
1037
+ const condition = this.expr();
1038
+ this.eat('THEN');
1039
+ const thenBranch = this.expr();
1040
+ this.eat('ELSE');
1041
+ const elseBranch = this.expr();
1042
+ return (0, ast_1.ifExpr)(condition, thenBranch, elseBranch);
1043
+ }
1044
+ /**
1045
+ * Parse lambda expression: fn( ~> body ) or fn( x ~> body ) or fn( x, y ~> body )
1046
+ */
1047
+ lambdaParse() {
1048
+ this.eat('FN');
1049
+ this.eat('LPAREN');
1050
+ const params = [];
1051
+ // Check for parameterless lambda: fn( ~> body )
1052
+ if (this.currentToken.type !== 'ARROW') {
1053
+ // Parse first parameter
1054
+ const firstName = this.currentToken.value;
1055
+ this.eat('IDENTIFIER');
1056
+ params.push(firstName);
1057
+ // Parse additional parameters
1058
+ while (this.currentToken.type === 'COMMA') {
1059
+ this.eat('COMMA');
1060
+ const name = this.currentToken.value;
1061
+ this.eat('IDENTIFIER');
1062
+ params.push(name);
1063
+ }
1064
+ }
1065
+ this.eat('ARROW');
1066
+ const body = this.expr();
1067
+ this.eat('RPAREN');
1068
+ return (0, ast_1.lambda)(params, body);
1069
+ }
1070
+ /**
1071
+ * Parse object literal: {key: value, key2: value2, ...}
1072
+ */
1073
+ objectParse() {
1074
+ this.eat('LBRACE');
1075
+ const properties = [];
1076
+ // Handle empty object
1077
+ if (this.currentToken.type === 'RBRACE') {
1078
+ this.eat('RBRACE');
1079
+ return (0, ast_1.objectLiteral)(properties);
1080
+ }
1081
+ // Parse first property
1082
+ const firstName = this.currentToken.value;
1083
+ this.eat('IDENTIFIER');
1084
+ this.eat('COLON');
1085
+ const firstValue = this.expr();
1086
+ properties.push({ key: firstName, value: firstValue });
1087
+ // Parse additional properties
1088
+ while (this.currentToken.type === 'COMMA') {
1089
+ this.eat('COMMA');
1090
+ const name = this.currentToken.value;
1091
+ this.eat('IDENTIFIER');
1092
+ this.eat('COLON');
1093
+ const value = this.expr();
1094
+ properties.push({ key: name, value });
1095
+ }
1096
+ this.eat('RBRACE');
1097
+ return (0, ast_1.objectLiteral)(properties);
1098
+ }
1099
+ /**
1100
+ * Parse array literal: [expr, expr, ...]
1101
+ */
1102
+ arrayParse() {
1103
+ this.eat('LBRACKET');
1104
+ const elements = [];
1105
+ // Handle empty array
1106
+ if (this.currentToken.type === 'RBRACKET') {
1107
+ this.eat('RBRACKET');
1108
+ return (0, ast_1.arrayLiteral)(elements);
1109
+ }
1110
+ // Parse first element
1111
+ elements.push(this.expr());
1112
+ // Parse additional elements
1113
+ while (this.currentToken.type === 'COMMA') {
1114
+ this.eat('COMMA');
1115
+ elements.push(this.expr());
1116
+ }
1117
+ this.eat('RBRACKET');
1118
+ return (0, ast_1.arrayLiteral)(elements);
1119
+ }
1120
+ /**
1121
+ * Parse datapath literal: .x.y.z or .items.0.name
1122
+ * Grammar: '.' pathSegment ('.' pathSegment)*
1123
+ * pathSegment: IDENTIFIER | NUMBER
1124
+ *
1125
+ * Note: The lexer may tokenize "0.1" as a single NUMBER token when parsing
1126
+ * consecutive numeric segments. We handle this by splitting such tokens.
1127
+ * The lexer also treats ".0" as a NUMBER token (decimal number starting with dot).
1128
+ */
1129
+ datapathParse() {
1130
+ this.eat('DOT');
1131
+ const segments = [];
1132
+ // Parse first segment (required) - could be IDENTIFIER or NUMBER
1133
+ if (this.currentToken.type === 'IDENTIFIER') {
1134
+ segments.push(this.currentToken.value);
1135
+ this.eat('IDENTIFIER');
1136
+ }
1137
+ else if (this.currentToken.type === 'NUMBER') {
1138
+ // NUMBER token might contain multiple segments (e.g., "0.1" -> [0, 1])
1139
+ const numSegments = this.parseNumericPathSegments();
1140
+ if (numSegments === null || numSegments.length === 0) {
1141
+ throw new Error(`Expected identifier or number after '.' at ${this.formatLocation(this.currentToken)}`);
1142
+ }
1143
+ segments.push(...numSegments);
1144
+ }
1145
+ else {
1146
+ throw new Error(`Expected identifier or number after '.' at ${this.formatLocation(this.currentToken)}`);
1147
+ }
1148
+ // Parse additional segments
1149
+ this.parseAdditionalPathSegments(segments);
1150
+ return (0, ast_1.dataPath)(segments);
1151
+ }
1152
+ /**
1153
+ * Parse additional path segments after the first one
1154
+ */
1155
+ parseAdditionalPathSegments(segments) {
1156
+ while (true) {
1157
+ // Get a fresh reference to avoid TypeScript control flow narrowing issues
1158
+ const token = this.currentToken;
1159
+ if (token.type === 'DOT') {
1160
+ this.eat('DOT');
1161
+ // After a DOT, we expect IDENTIFIER or NUMBER
1162
+ const nextToken = this.currentToken;
1163
+ if (nextToken.type === 'IDENTIFIER') {
1164
+ segments.push(nextToken.value);
1165
+ this.eat('IDENTIFIER');
1166
+ }
1167
+ else if (nextToken.type === 'NUMBER') {
1168
+ const numSegments = this.parseNumericPathSegments();
1169
+ if (numSegments === null || numSegments.length === 0) {
1170
+ throw new Error(`Expected identifier or number after '.' at ${this.formatLocation(this.currentToken)}`);
1171
+ }
1172
+ segments.push(...numSegments);
1173
+ }
1174
+ else {
1175
+ throw new Error(`Expected identifier or number after '.' at ${this.formatLocation(this.currentToken)}`);
1176
+ }
1177
+ }
1178
+ else if (token.type === 'NUMBER' && token.value.startsWith('.')) {
1179
+ // Handle NUMBER tokens that start with "." (e.g., ".0" tokenized as decimal)
1180
+ const numValue = token.value;
1181
+ this.eat('NUMBER');
1182
+ // Skip the leading dot and split on remaining dots
1183
+ const withoutLeadingDot = numValue.substring(1);
1184
+ const parts = withoutLeadingDot.split('.');
1185
+ for (const part of parts) {
1186
+ if (part.length > 0) {
1187
+ segments.push(parseInt(part, 10));
1188
+ }
1189
+ }
1190
+ }
1191
+ else {
1192
+ break;
1193
+ }
1194
+ }
1195
+ }
1196
+ /**
1197
+ * Check if current token is a NUMBER that starts with a dot (e.g., ".0")
1198
+ * This happens when the lexer treats ".0" as a decimal number
1199
+ */
1200
+ isDecimalNumberToken() {
1201
+ return this.currentToken.type === 'NUMBER' && this.currentToken.value.startsWith('.');
1202
+ }
1203
+ /**
1204
+ * Parse path segments from a NUMBER token.
1205
+ * A NUMBER token like "0.1" in datapath context should become segments [0, 1].
1206
+ * Returns the array of integer segments, or null if not a valid NUMBER.
1207
+ */
1208
+ parseNumericPathSegments() {
1209
+ if (this.currentToken.type !== 'NUMBER') {
1210
+ return null;
1211
+ }
1212
+ const tokenValue = this.currentToken.value;
1213
+ this.eat('NUMBER');
1214
+ // Split on decimal points to get individual integer segments
1215
+ const parts = tokenValue.split('.');
1216
+ return parts.filter(p => p.length > 0).map(p => parseInt(p, 10));
1217
+ }
1218
+ /**
1219
+ * Parse a single path segment: IDENTIFIER or integer NUMBER
1220
+ * Returns null if current token is not a valid segment.
1221
+ */
1222
+ parsePathSegment() {
1223
+ if (this.currentToken.type === 'IDENTIFIER') {
1224
+ const value = this.currentToken.value;
1225
+ this.eat('IDENTIFIER');
1226
+ return value;
1227
+ }
1228
+ else if (this.currentToken.type === 'NUMBER') {
1229
+ const value = parseInt(this.currentToken.value, 10);
1230
+ this.eat('NUMBER');
1231
+ return value;
1232
+ }
1233
+ return null;
1234
+ }
1235
+ expr() {
1236
+ this.depth++;
1237
+ this.checkDepth();
1238
+ try {
1239
+ // Let and if expressions have lowest precedence
1240
+ if (this.currentToken.type === 'LET') {
1241
+ return this.letExpr();
1242
+ }
1243
+ if (this.currentToken.type === 'IF') {
1244
+ return this.ifExprParse();
1245
+ }
1246
+ return this.pipe();
1247
+ }
1248
+ finally {
1249
+ this.depth--;
1250
+ }
1251
+ }
1252
+ parse() {
1253
+ const result = this.expr();
1254
+ this.eat('EOF');
1255
+ return result;
1256
+ }
1257
+ }
1258
+ exports.Parser = Parser;
1259
+ /**
1260
+ * Parse an arithmetic expression string into an AST
1261
+ */
1262
+ function parse(input, options = {}) {
1263
+ const parser = new Parser(input, options);
1264
+ return parser.parse();
1265
+ }
1266
+ //# sourceMappingURL=parser.js.map