@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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/bin/elo +2 -0
- package/bin/eloc +2 -0
- package/dist/src/ast.d.ts +309 -0
- package/dist/src/ast.d.ts.map +1 -0
- package/dist/src/ast.js +173 -0
- package/dist/src/ast.js.map +1 -0
- package/dist/src/bindings/javascript.d.ts +17 -0
- package/dist/src/bindings/javascript.d.ts.map +1 -0
- package/dist/src/bindings/javascript.js +350 -0
- package/dist/src/bindings/javascript.js.map +1 -0
- package/dist/src/bindings/ruby.d.ts +20 -0
- package/dist/src/bindings/ruby.d.ts.map +1 -0
- package/dist/src/bindings/ruby.js +365 -0
- package/dist/src/bindings/ruby.js.map +1 -0
- package/dist/src/bindings/sql.d.ts +20 -0
- package/dist/src/bindings/sql.d.ts.map +1 -0
- package/dist/src/bindings/sql.js +319 -0
- package/dist/src/bindings/sql.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +225 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/compile.d.ts +47 -0
- package/dist/src/compile.d.ts.map +1 -0
- package/dist/src/compile.js +55 -0
- package/dist/src/compile.js.map +1 -0
- package/dist/src/compilers/javascript.d.ts +41 -0
- package/dist/src/compilers/javascript.d.ts.map +1 -0
- package/dist/src/compilers/javascript.js +323 -0
- package/dist/src/compilers/javascript.js.map +1 -0
- package/dist/src/compilers/ruby.d.ts +40 -0
- package/dist/src/compilers/ruby.d.ts.map +1 -0
- package/dist/src/compilers/ruby.js +326 -0
- package/dist/src/compilers/ruby.js.map +1 -0
- package/dist/src/compilers/sql.d.ts +37 -0
- package/dist/src/compilers/sql.d.ts.map +1 -0
- package/dist/src/compilers/sql.js +164 -0
- package/dist/src/compilers/sql.js.map +1 -0
- package/dist/src/elo.d.ts +3 -0
- package/dist/src/elo.d.ts.map +1 -0
- package/dist/src/elo.js +187 -0
- package/dist/src/elo.js.map +1 -0
- package/dist/src/eloc.d.ts +3 -0
- package/dist/src/eloc.d.ts.map +1 -0
- package/dist/src/eloc.js +232 -0
- package/dist/src/eloc.js.map +1 -0
- package/dist/src/eval.d.ts +3 -0
- package/dist/src/eval.d.ts.map +1 -0
- package/dist/src/eval.js +196 -0
- package/dist/src/eval.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +36 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/ir.d.ts +295 -0
- package/dist/src/ir.d.ts.map +1 -0
- package/dist/src/ir.js +224 -0
- package/dist/src/ir.js.map +1 -0
- package/dist/src/parser.d.ts +137 -0
- package/dist/src/parser.d.ts.map +1 -0
- package/dist/src/parser.js +1266 -0
- package/dist/src/parser.js.map +1 -0
- package/dist/src/preludes/index.d.ts +14 -0
- package/dist/src/preludes/index.d.ts.map +1 -0
- package/dist/src/preludes/index.js +27 -0
- package/dist/src/preludes/index.js.map +1 -0
- package/dist/src/runtime.d.ts +23 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +326 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/stdlib.d.ts +121 -0
- package/dist/src/stdlib.d.ts.map +1 -0
- package/dist/src/stdlib.js +237 -0
- package/dist/src/stdlib.js.map +1 -0
- package/dist/src/transform.d.ts +38 -0
- package/dist/src/transform.d.ts.map +1 -0
- package/dist/src/transform.js +322 -0
- package/dist/src/transform.js.map +1 -0
- package/dist/src/typedefs.d.ts +50 -0
- package/dist/src/typedefs.d.ts.map +1 -0
- package/dist/src/typedefs.js +294 -0
- package/dist/src/typedefs.js.map +1 -0
- package/dist/src/types.d.ts +54 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +62 -0
- package/dist/src/types.js.map +1 -0
- 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
|