@ahsankhanamu/json-transformer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ast.d.ts +259 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +12 -0
- package/dist/ast.js.map +1 -0
- package/dist/codegen/base.d.ts +83 -0
- package/dist/codegen/base.d.ts.map +1 -0
- package/dist/codegen/base.js +494 -0
- package/dist/codegen/base.js.map +1 -0
- package/dist/codegen/index.d.ts +35 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +42 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/library.d.ts +42 -0
- package/dist/codegen/library.d.ts.map +1 -0
- package/dist/codegen/library.js +406 -0
- package/dist/codegen/library.js.map +1 -0
- package/dist/codegen/native.d.ts +33 -0
- package/dist/codegen/native.d.ts.map +1 -0
- package/dist/codegen/native.js +452 -0
- package/dist/codegen/native.js.map +1 -0
- package/dist/codegen.d.ts +13 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +12 -0
- package/dist/codegen.js.map +1 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +231 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +37 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +499 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +63 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1045 -0
- package/dist/parser.js.map +1 -0
- package/dist/playground.d.ts +6 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +216 -0
- package/dist/playground.js.map +1 -0
- package/dist/runtime.d.ts +229 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +933 -0
- package/dist/runtime.js.map +1 -0
- package/dist/tokens.d.ts +80 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +88 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +30 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser - Recursive Descent Parser
|
|
3
|
+
* Produces AST from tokens
|
|
4
|
+
*/
|
|
5
|
+
import { TokenType } from './tokens.js';
|
|
6
|
+
import { Lexer } from './lexer.js';
|
|
7
|
+
export class ParseError extends Error {
|
|
8
|
+
token;
|
|
9
|
+
expected;
|
|
10
|
+
constructor(message, token, expected) {
|
|
11
|
+
super(`Parse error at line ${token.line}, column ${token.column}: ${message}`);
|
|
12
|
+
this.token = token;
|
|
13
|
+
this.expected = expected;
|
|
14
|
+
this.name = 'ParseError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class Parser {
|
|
18
|
+
input;
|
|
19
|
+
tokens = [];
|
|
20
|
+
current = 0;
|
|
21
|
+
inPipeObjectContext = false;
|
|
22
|
+
// Stack of arrow function parameter names (for nested arrows)
|
|
23
|
+
// When inside an arrow body, .property resolves to param.property
|
|
24
|
+
arrowParamStack = [];
|
|
25
|
+
constructor(input) {
|
|
26
|
+
this.input = input;
|
|
27
|
+
}
|
|
28
|
+
parse() {
|
|
29
|
+
const lexer = new Lexer(this.input);
|
|
30
|
+
this.tokens = lexer.tokenize();
|
|
31
|
+
this.current = 0;
|
|
32
|
+
const statements = [];
|
|
33
|
+
let expression = null;
|
|
34
|
+
// Parse statements (let/const bindings)
|
|
35
|
+
while (!this.isAtEnd() && (this.check(TokenType.LET) || this.check(TokenType.CONST))) {
|
|
36
|
+
statements.push(this.parseLetBinding());
|
|
37
|
+
}
|
|
38
|
+
// Parse final expression
|
|
39
|
+
if (!this.isAtEnd()) {
|
|
40
|
+
expression = this.parseExpression();
|
|
41
|
+
}
|
|
42
|
+
// Should be at EOF
|
|
43
|
+
if (!this.isAtEnd()) {
|
|
44
|
+
throw new ParseError('Unexpected token after expression', this.peek());
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
type: 'Program',
|
|
48
|
+
statements,
|
|
49
|
+
expression,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ===========================================================================
|
|
53
|
+
// STATEMENTS
|
|
54
|
+
// ===========================================================================
|
|
55
|
+
parseLetBinding() {
|
|
56
|
+
const constant = this.check(TokenType.CONST);
|
|
57
|
+
this.advance(); // consume let/const
|
|
58
|
+
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected variable name');
|
|
59
|
+
this.consume(TokenType.ASSIGN, 'Expected "=" after variable name');
|
|
60
|
+
const value = this.parseExpression();
|
|
61
|
+
this.consume(TokenType.SEMICOLON, 'Expected ";" after variable declaration');
|
|
62
|
+
return {
|
|
63
|
+
type: 'LetBinding',
|
|
64
|
+
name: nameToken.value,
|
|
65
|
+
value,
|
|
66
|
+
constant,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ===========================================================================
|
|
70
|
+
// EXPRESSIONS (by precedence, lowest to highest)
|
|
71
|
+
// ===========================================================================
|
|
72
|
+
parseExpression() {
|
|
73
|
+
return this.parsePipe();
|
|
74
|
+
}
|
|
75
|
+
// Pipe: value | transform | transform
|
|
76
|
+
// Also supports jq-style property access: value | .field | .[0] | .method()
|
|
77
|
+
// And pipe-to-object/array construction: value | { id: .id } or value | [.id, .name]
|
|
78
|
+
parsePipe() {
|
|
79
|
+
let left = this.parseTernary();
|
|
80
|
+
while (this.match(TokenType.PIPE)) {
|
|
81
|
+
// Check if next token is DOT - pipe property access syntax (jq-style)
|
|
82
|
+
if (this.check(TokenType.DOT)) {
|
|
83
|
+
const right = this.parsePipePropertyAccess();
|
|
84
|
+
left = { type: 'PipeExpression', left, right };
|
|
85
|
+
}
|
|
86
|
+
else if (this.check(TokenType.LBRACKET)) {
|
|
87
|
+
// Could be index access [0] or array construction [.id, .name]
|
|
88
|
+
const right = this.parsePipeArrayOrIndex();
|
|
89
|
+
left = { type: 'PipeExpression', left, right };
|
|
90
|
+
}
|
|
91
|
+
else if (this.check(TokenType.LBRACE)) {
|
|
92
|
+
// Pipe-to-object construction: value | { .field, newKey: .other }
|
|
93
|
+
const right = this.parsePipeObjectConstruction();
|
|
94
|
+
left = { type: 'PipeExpression', left, right };
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const right = this.parseTernary();
|
|
98
|
+
left = { type: 'PipeExpression', left, right };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return left;
|
|
102
|
+
}
|
|
103
|
+
// Determine if | [...] is array construction or index access
|
|
104
|
+
// Array construction: | [.id, .name] or | [id, name] (shorthand) or | [expr, expr, ...]
|
|
105
|
+
// Index access: | [0] or | [expr] (single non-identifier expression)
|
|
106
|
+
parsePipeArrayOrIndex() {
|
|
107
|
+
// Peek at what follows [
|
|
108
|
+
const afterBracket = this.peekNext(); // token after [
|
|
109
|
+
// If [ is followed by DOT, it's array construction with pipe context refs: [.id, .name]
|
|
110
|
+
if (afterBracket.type === TokenType.DOT) {
|
|
111
|
+
return this.parsePipeArrayConstruction();
|
|
112
|
+
}
|
|
113
|
+
// If [ is followed by ], it's empty array construction: []
|
|
114
|
+
if (afterBracket.type === TokenType.RBRACKET) {
|
|
115
|
+
return this.parsePipeArrayConstruction();
|
|
116
|
+
}
|
|
117
|
+
// If [ is followed by SPREAD, it's array construction: [...items]
|
|
118
|
+
if (afterBracket.type === TokenType.SPREAD) {
|
|
119
|
+
return this.parsePipeArrayConstruction();
|
|
120
|
+
}
|
|
121
|
+
// If [ is followed by identifier, check if comma follows (array) or ] follows (index)
|
|
122
|
+
// For now, if followed by identifier, treat as array construction (shorthand)
|
|
123
|
+
if (afterBracket.type === TokenType.IDENTIFIER) {
|
|
124
|
+
return this.parsePipeArrayConstruction();
|
|
125
|
+
}
|
|
126
|
+
// Otherwise (number, string, expression), treat as index access
|
|
127
|
+
return this.parsePipeIndexAccess();
|
|
128
|
+
}
|
|
129
|
+
// Parse pipe-to-object construction: value | { id: .id, name: .name | upper }
|
|
130
|
+
parsePipeObjectConstruction() {
|
|
131
|
+
const savedContext = this.inPipeObjectContext;
|
|
132
|
+
this.inPipeObjectContext = true;
|
|
133
|
+
this.advance(); // consume {
|
|
134
|
+
const result = this.parseObjectLiteral();
|
|
135
|
+
this.inPipeObjectContext = savedContext;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
// Parse pipe-to-array construction: value | [.id, .name] or value | [id, name]
|
|
139
|
+
parsePipeArrayConstruction() {
|
|
140
|
+
const savedContext = this.inPipeObjectContext;
|
|
141
|
+
this.inPipeObjectContext = true;
|
|
142
|
+
this.advance(); // consume [
|
|
143
|
+
const result = this.parsePipeArrayLiteral();
|
|
144
|
+
this.inPipeObjectContext = savedContext;
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
// Parse array literal in pipe context - identifiers become pipe context refs
|
|
148
|
+
parsePipeArrayLiteral() {
|
|
149
|
+
const elements = [];
|
|
150
|
+
while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
|
|
151
|
+
if (this.match(TokenType.SPREAD)) {
|
|
152
|
+
// Spread in pipe array context: [...] spreads pipe value, [...expr] spreads expr
|
|
153
|
+
if (this.check(TokenType.COMMA) || this.check(TokenType.RBRACKET)) {
|
|
154
|
+
// Bare spread: [...] spreads the pipe context
|
|
155
|
+
elements.push({ type: 'SpreadElement', argument: { type: 'PipeContextRef' } });
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const argument = this.parseExpression();
|
|
159
|
+
elements.push({ type: 'SpreadElement', argument });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (this.match(TokenType.IDENTIFIER)) {
|
|
163
|
+
// Shorthand: identifier in pipe array context becomes .identifier
|
|
164
|
+
const name = this.previous().value;
|
|
165
|
+
elements.push({
|
|
166
|
+
type: 'MemberAccess',
|
|
167
|
+
object: { type: 'PipeContextRef' },
|
|
168
|
+
property: name,
|
|
169
|
+
optional: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
elements.push(this.parseExpression());
|
|
174
|
+
}
|
|
175
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
176
|
+
this.consume(TokenType.COMMA, 'Expected "," between elements');
|
|
177
|
+
// Allow trailing comma
|
|
178
|
+
if (this.check(TokenType.RBRACKET))
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
this.consume(TokenType.RBRACKET, 'Expected "]" after array literal');
|
|
183
|
+
return { type: 'ArrayLiteral', elements };
|
|
184
|
+
}
|
|
185
|
+
// Parse jq-style pipe property access: .field, .[0], .field.subfield, .method()
|
|
186
|
+
parsePipePropertyAccess() {
|
|
187
|
+
// Start with PipeContextRef as the base
|
|
188
|
+
let expr = { type: 'PipeContextRef' };
|
|
189
|
+
// Must start with DOT
|
|
190
|
+
while (this.check(TokenType.DOT) || this.check(TokenType.LBRACKET)) {
|
|
191
|
+
if (this.match(TokenType.DOT)) {
|
|
192
|
+
// Check for .[index] syntax
|
|
193
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
194
|
+
this.advance(); // consume [
|
|
195
|
+
const index = this.parseExpression();
|
|
196
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
197
|
+
expr = {
|
|
198
|
+
type: 'IndexAccess',
|
|
199
|
+
object: expr,
|
|
200
|
+
index,
|
|
201
|
+
optional: false,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// .field or .method()
|
|
206
|
+
const name = this.consume(TokenType.IDENTIFIER, 'Expected property name after "."');
|
|
207
|
+
expr = {
|
|
208
|
+
type: 'MemberAccess',
|
|
209
|
+
object: expr,
|
|
210
|
+
property: name.value,
|
|
211
|
+
optional: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (this.match(TokenType.LBRACKET)) {
|
|
216
|
+
// [0] or ["key"] without dot prefix
|
|
217
|
+
const index = this.parseExpression();
|
|
218
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
219
|
+
expr = {
|
|
220
|
+
type: 'IndexAccess',
|
|
221
|
+
object: expr,
|
|
222
|
+
index,
|
|
223
|
+
optional: false,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// Handle method calls: .method()
|
|
227
|
+
if (this.check(TokenType.LPAREN)) {
|
|
228
|
+
this.advance();
|
|
229
|
+
const args = [];
|
|
230
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
231
|
+
do {
|
|
232
|
+
args.push(this.parseExpression());
|
|
233
|
+
} while (this.match(TokenType.COMMA));
|
|
234
|
+
}
|
|
235
|
+
this.consume(TokenType.RPAREN, 'Expected ")"');
|
|
236
|
+
expr = {
|
|
237
|
+
type: 'CallExpression',
|
|
238
|
+
callee: expr,
|
|
239
|
+
arguments: args,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return expr;
|
|
244
|
+
}
|
|
245
|
+
// Parse pipe index access: | [0] (without dot)
|
|
246
|
+
parsePipeIndexAccess() {
|
|
247
|
+
let expr = { type: 'PipeContextRef' };
|
|
248
|
+
this.advance(); // consume [
|
|
249
|
+
const index = this.parseExpression();
|
|
250
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
251
|
+
expr = {
|
|
252
|
+
type: 'IndexAccess',
|
|
253
|
+
object: expr,
|
|
254
|
+
index,
|
|
255
|
+
optional: false,
|
|
256
|
+
};
|
|
257
|
+
// Allow chaining after: | [0].field or | [0][1]
|
|
258
|
+
while (this.check(TokenType.DOT) || this.check(TokenType.LBRACKET)) {
|
|
259
|
+
if (this.match(TokenType.DOT)) {
|
|
260
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
261
|
+
this.advance();
|
|
262
|
+
const idx = this.parseExpression();
|
|
263
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
264
|
+
expr = { type: 'IndexAccess', object: expr, index: idx, optional: false };
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const name = this.consume(TokenType.IDENTIFIER, 'Expected property name after "."');
|
|
268
|
+
expr = {
|
|
269
|
+
type: 'MemberAccess',
|
|
270
|
+
object: expr,
|
|
271
|
+
property: name.value,
|
|
272
|
+
optional: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (this.match(TokenType.LBRACKET)) {
|
|
277
|
+
const idx = this.parseExpression();
|
|
278
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
279
|
+
expr = { type: 'IndexAccess', object: expr, index: idx, optional: false };
|
|
280
|
+
}
|
|
281
|
+
// Handle method calls
|
|
282
|
+
if (this.check(TokenType.LPAREN)) {
|
|
283
|
+
this.advance();
|
|
284
|
+
const args = [];
|
|
285
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
286
|
+
do {
|
|
287
|
+
args.push(this.parseExpression());
|
|
288
|
+
} while (this.match(TokenType.COMMA));
|
|
289
|
+
}
|
|
290
|
+
this.consume(TokenType.RPAREN, 'Expected ")"');
|
|
291
|
+
expr = { type: 'CallExpression', callee: expr, arguments: args };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return expr;
|
|
295
|
+
}
|
|
296
|
+
// Parse .field access inside pipe object context (DOT already consumed)
|
|
297
|
+
// Returns PipeContextRef-based expression: .field, .[0], .field.nested
|
|
298
|
+
parsePipeContextAccess() {
|
|
299
|
+
let expr = { type: 'PipeContextRef' };
|
|
300
|
+
// Check for .[index] syntax
|
|
301
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
302
|
+
this.advance(); // consume [
|
|
303
|
+
const index = this.parseExpression();
|
|
304
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
305
|
+
expr = { type: 'IndexAccess', object: expr, index, optional: false };
|
|
306
|
+
}
|
|
307
|
+
else if (this.check(TokenType.IDENTIFIER)) {
|
|
308
|
+
// .field
|
|
309
|
+
const name = this.advance();
|
|
310
|
+
expr = {
|
|
311
|
+
type: 'MemberAccess',
|
|
312
|
+
object: expr,
|
|
313
|
+
property: name.value,
|
|
314
|
+
optional: false,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// Just . by itself - return the pipe context
|
|
319
|
+
return expr;
|
|
320
|
+
}
|
|
321
|
+
// Allow chaining: .field.nested, .field[0], .field.method()
|
|
322
|
+
while (this.check(TokenType.DOT) || this.check(TokenType.LBRACKET)) {
|
|
323
|
+
if (this.match(TokenType.DOT)) {
|
|
324
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
325
|
+
this.advance();
|
|
326
|
+
const idx = this.parseExpression();
|
|
327
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
328
|
+
expr = { type: 'IndexAccess', object: expr, index: idx, optional: false };
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const name = this.consume(TokenType.IDENTIFIER, 'Expected property name after "."');
|
|
332
|
+
expr = {
|
|
333
|
+
type: 'MemberAccess',
|
|
334
|
+
object: expr,
|
|
335
|
+
property: name.value,
|
|
336
|
+
optional: false,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if (this.match(TokenType.LBRACKET)) {
|
|
341
|
+
const idx = this.parseExpression();
|
|
342
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
343
|
+
expr = { type: 'IndexAccess', object: expr, index: idx, optional: false };
|
|
344
|
+
}
|
|
345
|
+
// Handle method calls: .method()
|
|
346
|
+
if (this.check(TokenType.LPAREN)) {
|
|
347
|
+
this.advance();
|
|
348
|
+
const args = [];
|
|
349
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
350
|
+
do {
|
|
351
|
+
args.push(this.parseExpression());
|
|
352
|
+
} while (this.match(TokenType.COMMA));
|
|
353
|
+
}
|
|
354
|
+
this.consume(TokenType.RPAREN, 'Expected ")"');
|
|
355
|
+
expr = { type: 'CallExpression', callee: expr, arguments: args };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return expr;
|
|
359
|
+
}
|
|
360
|
+
// Parse .field access inside arrow function body (DOT already consumed)
|
|
361
|
+
// Returns param-based expression: .field becomes param.field, .[0] becomes param[0]
|
|
362
|
+
parseArrowContextAccess() {
|
|
363
|
+
const paramName = this.arrowParamStack[this.arrowParamStack.length - 1];
|
|
364
|
+
let expr = { type: 'Identifier', name: paramName };
|
|
365
|
+
// Check for .[index] syntax
|
|
366
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
367
|
+
this.advance(); // consume [
|
|
368
|
+
const index = this.parseExpression();
|
|
369
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
370
|
+
expr = { type: 'IndexAccess', object: expr, index, optional: false };
|
|
371
|
+
}
|
|
372
|
+
else if (this.check(TokenType.IDENTIFIER)) {
|
|
373
|
+
// .field
|
|
374
|
+
const name = this.advance();
|
|
375
|
+
expr = {
|
|
376
|
+
type: 'MemberAccess',
|
|
377
|
+
object: expr,
|
|
378
|
+
property: name.value,
|
|
379
|
+
optional: false,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Just . by itself - return the parameter identifier
|
|
384
|
+
return expr;
|
|
385
|
+
}
|
|
386
|
+
// Allow chaining: .field.nested, .field[0], .field.method()
|
|
387
|
+
while (this.check(TokenType.DOT) || this.check(TokenType.LBRACKET)) {
|
|
388
|
+
if (this.match(TokenType.DOT)) {
|
|
389
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
390
|
+
this.advance();
|
|
391
|
+
const idx = this.parseExpression();
|
|
392
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
393
|
+
expr = { type: 'IndexAccess', object: expr, index: idx, optional: false };
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const name = this.consume(TokenType.IDENTIFIER, 'Expected property name after "."');
|
|
397
|
+
expr = {
|
|
398
|
+
type: 'MemberAccess',
|
|
399
|
+
object: expr,
|
|
400
|
+
property: name.value,
|
|
401
|
+
optional: false,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (this.match(TokenType.LBRACKET)) {
|
|
406
|
+
const idx = this.parseExpression();
|
|
407
|
+
this.consume(TokenType.RBRACKET, 'Expected "]"');
|
|
408
|
+
expr = { type: 'IndexAccess', object: expr, index: idx, optional: false };
|
|
409
|
+
}
|
|
410
|
+
// Handle method calls: .method()
|
|
411
|
+
if (this.check(TokenType.LPAREN)) {
|
|
412
|
+
this.advance();
|
|
413
|
+
const args = [];
|
|
414
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
415
|
+
do {
|
|
416
|
+
args.push(this.parseExpression());
|
|
417
|
+
} while (this.match(TokenType.COMMA));
|
|
418
|
+
}
|
|
419
|
+
this.consume(TokenType.RPAREN, 'Expected ")"');
|
|
420
|
+
expr = { type: 'CallExpression', callee: expr, arguments: args };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return expr;
|
|
424
|
+
}
|
|
425
|
+
// Ternary: condition ? then : else
|
|
426
|
+
parseTernary() {
|
|
427
|
+
const test = this.parseLogicalOr();
|
|
428
|
+
if (this.match(TokenType.QUESTION)) {
|
|
429
|
+
const consequent = this.parseExpression();
|
|
430
|
+
this.consume(TokenType.COLON, 'Expected ":" in ternary expression');
|
|
431
|
+
const alternate = this.parseTernary();
|
|
432
|
+
return {
|
|
433
|
+
type: 'TernaryExpression',
|
|
434
|
+
test,
|
|
435
|
+
consequent,
|
|
436
|
+
alternate,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return test;
|
|
440
|
+
}
|
|
441
|
+
// Logical OR: a || b, a or b
|
|
442
|
+
parseLogicalOr() {
|
|
443
|
+
let left = this.parseLogicalAnd();
|
|
444
|
+
while (this.match(TokenType.OR_OR) || this.match(TokenType.OR)) {
|
|
445
|
+
const _operator = this.previous().value;
|
|
446
|
+
const right = this.parseLogicalAnd();
|
|
447
|
+
left = { type: 'BinaryExpression', operator: '||', left, right };
|
|
448
|
+
}
|
|
449
|
+
return left;
|
|
450
|
+
}
|
|
451
|
+
// Logical AND: a && b, a and b
|
|
452
|
+
parseLogicalAnd() {
|
|
453
|
+
let left = this.parseEquality();
|
|
454
|
+
while (this.match(TokenType.AND_AND) || this.match(TokenType.AND)) {
|
|
455
|
+
const _operator = this.previous().value;
|
|
456
|
+
const right = this.parseEquality();
|
|
457
|
+
left = { type: 'BinaryExpression', operator: '&&', left, right };
|
|
458
|
+
}
|
|
459
|
+
return left;
|
|
460
|
+
}
|
|
461
|
+
// Equality: a == b, a != b
|
|
462
|
+
parseEquality() {
|
|
463
|
+
let left = this.parseComparison();
|
|
464
|
+
while (this.match(TokenType.EQ) ||
|
|
465
|
+
this.match(TokenType.NEQ) ||
|
|
466
|
+
this.match(TokenType.STRICT_EQ) ||
|
|
467
|
+
this.match(TokenType.STRICT_NEQ)) {
|
|
468
|
+
const operator = this.previous().value;
|
|
469
|
+
const right = this.parseComparison();
|
|
470
|
+
left = { type: 'BinaryExpression', operator, left, right };
|
|
471
|
+
}
|
|
472
|
+
return left;
|
|
473
|
+
}
|
|
474
|
+
// Comparison: a < b, a >= b, a in b
|
|
475
|
+
parseComparison() {
|
|
476
|
+
let left = this.parseNullCoalesce();
|
|
477
|
+
while (this.match(TokenType.LT) ||
|
|
478
|
+
this.match(TokenType.GT) ||
|
|
479
|
+
this.match(TokenType.LTE) ||
|
|
480
|
+
this.match(TokenType.GTE) ||
|
|
481
|
+
this.match(TokenType.IN)) {
|
|
482
|
+
const operator = this.previous().value;
|
|
483
|
+
const right = this.parseNullCoalesce();
|
|
484
|
+
left = { type: 'BinaryExpression', operator, left, right };
|
|
485
|
+
}
|
|
486
|
+
return left;
|
|
487
|
+
}
|
|
488
|
+
// Null coalescing: a ?? b
|
|
489
|
+
parseNullCoalesce() {
|
|
490
|
+
let left = this.parseAdditive();
|
|
491
|
+
while (this.match(TokenType.QUESTION_QUESTION)) {
|
|
492
|
+
const right = this.parseAdditive();
|
|
493
|
+
left = { type: 'NullCoalesce', left, right };
|
|
494
|
+
}
|
|
495
|
+
return left;
|
|
496
|
+
}
|
|
497
|
+
// Addition/Subtraction/Concatenation: a + b, a - b, a & b
|
|
498
|
+
parseAdditive() {
|
|
499
|
+
let left = this.parseMultiplicative();
|
|
500
|
+
while (this.match(TokenType.PLUS) ||
|
|
501
|
+
this.match(TokenType.MINUS) ||
|
|
502
|
+
this.match(TokenType.AMPERSAND)) {
|
|
503
|
+
const operator = this.previous().value;
|
|
504
|
+
const right = this.parseMultiplicative();
|
|
505
|
+
left = { type: 'BinaryExpression', operator, left, right };
|
|
506
|
+
}
|
|
507
|
+
return left;
|
|
508
|
+
}
|
|
509
|
+
// Multiplication/Division: a * b, a / b, a % b
|
|
510
|
+
parseMultiplicative() {
|
|
511
|
+
let left = this.parseUnary();
|
|
512
|
+
while (this.match(TokenType.STAR) ||
|
|
513
|
+
this.match(TokenType.SLASH) ||
|
|
514
|
+
this.match(TokenType.PERCENT)) {
|
|
515
|
+
const operator = this.previous().value;
|
|
516
|
+
const right = this.parseUnary();
|
|
517
|
+
left = { type: 'BinaryExpression', operator, left, right };
|
|
518
|
+
}
|
|
519
|
+
return left;
|
|
520
|
+
}
|
|
521
|
+
// Unary: !a, -a, +a, not a
|
|
522
|
+
parseUnary() {
|
|
523
|
+
if (this.match(TokenType.BANG) ||
|
|
524
|
+
this.match(TokenType.MINUS) ||
|
|
525
|
+
this.match(TokenType.PLUS) ||
|
|
526
|
+
this.match(TokenType.NOT)) {
|
|
527
|
+
const operator = this.previous().value;
|
|
528
|
+
const argument = this.parseUnary();
|
|
529
|
+
return { type: 'UnaryExpression', operator, argument, prefix: true };
|
|
530
|
+
}
|
|
531
|
+
return this.parsePostfix();
|
|
532
|
+
}
|
|
533
|
+
// Postfix: member access, index, call, etc.
|
|
534
|
+
parsePostfix() {
|
|
535
|
+
let expr = this.parsePrimary();
|
|
536
|
+
while (true) {
|
|
537
|
+
if (this.match(TokenType.DOT)) {
|
|
538
|
+
// Special case: [*].{ } for array mapping with object construction
|
|
539
|
+
if (expr.type === 'SpreadAccess' && this.match(TokenType.LBRACE)) {
|
|
540
|
+
const objectLiteral = this.parseObjectLiteral();
|
|
541
|
+
expr = { type: 'MapTransform', array: expr.object, template: objectLiteral };
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
const property = this.consume(TokenType.IDENTIFIER, 'Expected property name after "."');
|
|
545
|
+
expr = { type: 'MemberAccess', object: expr, property: property.value, optional: false };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else if (this.match(TokenType.QUESTION_DOT)) {
|
|
549
|
+
const property = this.consume(TokenType.IDENTIFIER, 'Expected property name after "?."');
|
|
550
|
+
expr = { type: 'MemberAccess', object: expr, property: property.value, optional: true };
|
|
551
|
+
}
|
|
552
|
+
else if (this.match(TokenType.LBRACKET)) {
|
|
553
|
+
expr = this.parseIndexOrSliceOrFilter(expr, false);
|
|
554
|
+
}
|
|
555
|
+
else if (this.match(TokenType.QUESTION_BRACKET)) {
|
|
556
|
+
expr = this.parseIndexOrSliceOrFilter(expr, true);
|
|
557
|
+
}
|
|
558
|
+
else if (this.match(TokenType.LPAREN)) {
|
|
559
|
+
expr = this.parseCallExpression(expr);
|
|
560
|
+
}
|
|
561
|
+
else if (this.match(TokenType.BANG)) {
|
|
562
|
+
// Non-null assertion
|
|
563
|
+
expr = { type: 'NonNullAssertion', expression: expr };
|
|
564
|
+
}
|
|
565
|
+
else if (this.match(TokenType.AS)) {
|
|
566
|
+
const typeAnnotation = this.parseTypeAnnotation();
|
|
567
|
+
expr = { type: 'TypeAssertion', expression: expr, typeAnnotation };
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return expr;
|
|
574
|
+
}
|
|
575
|
+
parseIndexOrSliceOrFilter(object, optional) {
|
|
576
|
+
// Check for spread: [*]
|
|
577
|
+
if (this.match(TokenType.STAR)) {
|
|
578
|
+
this.consume(TokenType.RBRACKET, 'Expected "]" after "*"');
|
|
579
|
+
return { type: 'SpreadAccess', object };
|
|
580
|
+
}
|
|
581
|
+
// Check for empty brackets [] - shorthand for [*]
|
|
582
|
+
if (this.match(TokenType.RBRACKET)) {
|
|
583
|
+
return { type: 'SpreadAccess', object };
|
|
584
|
+
}
|
|
585
|
+
// Check for filter: [? predicate] or just [predicate] where predicate is boolean
|
|
586
|
+
if (this.match(TokenType.QUESTION)) {
|
|
587
|
+
const predicate = this.parseExpression();
|
|
588
|
+
this.consume(TokenType.RBRACKET, 'Expected "]" after filter predicate');
|
|
589
|
+
return { type: 'FilterAccess', object, predicate };
|
|
590
|
+
}
|
|
591
|
+
// Check for slice: [start:end]
|
|
592
|
+
let start = null;
|
|
593
|
+
if (!this.check(TokenType.COLON) && !this.check(TokenType.RBRACKET)) {
|
|
594
|
+
start = this.parseExpression();
|
|
595
|
+
}
|
|
596
|
+
if (this.match(TokenType.COLON)) {
|
|
597
|
+
let end = null;
|
|
598
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
599
|
+
end = this.parseExpression();
|
|
600
|
+
}
|
|
601
|
+
this.consume(TokenType.RBRACKET, 'Expected "]" after slice');
|
|
602
|
+
return { type: 'SliceAccess', object, sliceStart: start, sliceEnd: end };
|
|
603
|
+
}
|
|
604
|
+
// Regular index access
|
|
605
|
+
this.consume(TokenType.RBRACKET, 'Expected "]" after index');
|
|
606
|
+
if (!start) {
|
|
607
|
+
// Safety fallback - shouldn't reach here since [] is handled above
|
|
608
|
+
return { type: 'SpreadAccess', object };
|
|
609
|
+
}
|
|
610
|
+
return { type: 'IndexAccess', object, index: start, optional };
|
|
611
|
+
}
|
|
612
|
+
parseCallExpression(callee) {
|
|
613
|
+
const args = [];
|
|
614
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
615
|
+
do {
|
|
616
|
+
args.push(this.parseExpression());
|
|
617
|
+
} while (this.match(TokenType.COMMA));
|
|
618
|
+
}
|
|
619
|
+
this.consume(TokenType.RPAREN, 'Expected ")" after arguments');
|
|
620
|
+
return { type: 'CallExpression', callee, arguments: args };
|
|
621
|
+
}
|
|
622
|
+
// ===========================================================================
|
|
623
|
+
// PRIMARY EXPRESSIONS
|
|
624
|
+
// ===========================================================================
|
|
625
|
+
parsePrimary() {
|
|
626
|
+
// Literals
|
|
627
|
+
if (this.match(TokenType.NUMBER)) {
|
|
628
|
+
return { type: 'NumberLiteral', value: parseFloat(this.previous().value) };
|
|
629
|
+
}
|
|
630
|
+
if (this.match(TokenType.STRING)) {
|
|
631
|
+
return { type: 'StringLiteral', value: this.previous().value };
|
|
632
|
+
}
|
|
633
|
+
if (this.match(TokenType.TEMPLATE_STRING)) {
|
|
634
|
+
return this.parseTemplateLiteral(this.previous().value);
|
|
635
|
+
}
|
|
636
|
+
if (this.match(TokenType.TRUE)) {
|
|
637
|
+
return { type: 'BooleanLiteral', value: true };
|
|
638
|
+
}
|
|
639
|
+
if (this.match(TokenType.FALSE)) {
|
|
640
|
+
return { type: 'BooleanLiteral', value: false };
|
|
641
|
+
}
|
|
642
|
+
if (this.match(TokenType.NULL)) {
|
|
643
|
+
return { type: 'NullLiteral' };
|
|
644
|
+
}
|
|
645
|
+
if (this.match(TokenType.UNDEFINED)) {
|
|
646
|
+
return { type: 'UndefinedLiteral' };
|
|
647
|
+
}
|
|
648
|
+
// Context access
|
|
649
|
+
if (this.match(TokenType.DOLLAR_DOLLAR)) {
|
|
650
|
+
this.consume(TokenType.DOT, 'Expected "." after "$$"');
|
|
651
|
+
const name = this.consume(TokenType.IDENTIFIER, 'Expected binding name');
|
|
652
|
+
return { type: 'BindingAccess', name: name.value };
|
|
653
|
+
}
|
|
654
|
+
if (this.match(TokenType.DOLLAR)) {
|
|
655
|
+
if (this.match(TokenType.DOT)) {
|
|
656
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
657
|
+
const name = this.advance();
|
|
658
|
+
return { type: 'RootAccess', path: name.value };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return { type: 'RootAccess', path: null };
|
|
662
|
+
}
|
|
663
|
+
if (this.match(TokenType.CARET)) {
|
|
664
|
+
if (this.match(TokenType.DOT)) {
|
|
665
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
666
|
+
const name = this.advance();
|
|
667
|
+
return { type: 'ParentAccess', path: name.value };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return { type: 'ParentAccess', path: null };
|
|
671
|
+
}
|
|
672
|
+
if (this.match(TokenType.DOT)) {
|
|
673
|
+
// In pipe object context, .field refers to pipe value (PipeContextRef)
|
|
674
|
+
if (this.inPipeObjectContext) {
|
|
675
|
+
return this.parsePipeContextAccess();
|
|
676
|
+
}
|
|
677
|
+
// In arrow function body, .field refers to the first parameter
|
|
678
|
+
// e.g., orders.find(o => .price > 10) means o.price > 10
|
|
679
|
+
if (this.arrowParamStack.length > 0) {
|
|
680
|
+
return this.parseArrowContextAccess();
|
|
681
|
+
}
|
|
682
|
+
// Otherwise, .field refers to current input (CurrentAccess)
|
|
683
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
684
|
+
const name = this.advance();
|
|
685
|
+
return { type: 'CurrentAccess', path: name.value };
|
|
686
|
+
}
|
|
687
|
+
return { type: 'CurrentAccess', path: null };
|
|
688
|
+
}
|
|
689
|
+
// Object literal
|
|
690
|
+
if (this.match(TokenType.LBRACE)) {
|
|
691
|
+
return this.parseObjectLiteral();
|
|
692
|
+
}
|
|
693
|
+
// Array literal
|
|
694
|
+
if (this.match(TokenType.LBRACKET)) {
|
|
695
|
+
return this.parseArrayLiteral();
|
|
696
|
+
}
|
|
697
|
+
// If expression
|
|
698
|
+
if (this.match(TokenType.IF)) {
|
|
699
|
+
return this.parseIfExpression();
|
|
700
|
+
}
|
|
701
|
+
// Parenthesized expression or arrow function
|
|
702
|
+
if (this.match(TokenType.LPAREN)) {
|
|
703
|
+
return this.parseParenOrArrow();
|
|
704
|
+
}
|
|
705
|
+
// Identifier (or potential arrow function with single param)
|
|
706
|
+
if (this.match(TokenType.IDENTIFIER)) {
|
|
707
|
+
const name = this.previous().value;
|
|
708
|
+
// Check for arrow function: identifier => expr
|
|
709
|
+
if (this.match(TokenType.ARROW)) {
|
|
710
|
+
// Push param to stack so .property inside body resolves to param.property
|
|
711
|
+
this.arrowParamStack.push(name);
|
|
712
|
+
const body = this.parseExpression();
|
|
713
|
+
this.arrowParamStack.pop();
|
|
714
|
+
return {
|
|
715
|
+
type: 'ArrowFunction',
|
|
716
|
+
params: [{ type: 'Parameter', name }],
|
|
717
|
+
body,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return { type: 'Identifier', name };
|
|
721
|
+
}
|
|
722
|
+
throw new ParseError(`Unexpected token: ${this.peek().value}`, this.peek());
|
|
723
|
+
}
|
|
724
|
+
parseTemplateLiteral(raw) {
|
|
725
|
+
const parts = [];
|
|
726
|
+
let current = '';
|
|
727
|
+
let i = 0;
|
|
728
|
+
while (i < raw.length) {
|
|
729
|
+
if (raw[i] === '$' && raw[i + 1] === '{') {
|
|
730
|
+
if (current) {
|
|
731
|
+
parts.push(current);
|
|
732
|
+
current = '';
|
|
733
|
+
}
|
|
734
|
+
// Find matching closing brace
|
|
735
|
+
let braceDepth = 1;
|
|
736
|
+
const exprStart = i + 2;
|
|
737
|
+
let j = exprStart;
|
|
738
|
+
while (j < raw.length && braceDepth > 0) {
|
|
739
|
+
if (raw[j] === '{')
|
|
740
|
+
braceDepth++;
|
|
741
|
+
if (raw[j] === '}')
|
|
742
|
+
braceDepth--;
|
|
743
|
+
j++;
|
|
744
|
+
}
|
|
745
|
+
const exprSource = raw.slice(exprStart, j - 1);
|
|
746
|
+
const exprParser = new Parser(exprSource);
|
|
747
|
+
const exprProgram = exprParser.parse();
|
|
748
|
+
if (exprProgram.expression) {
|
|
749
|
+
parts.push(exprProgram.expression);
|
|
750
|
+
}
|
|
751
|
+
i = j;
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
current += raw[i];
|
|
755
|
+
i++;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (current) {
|
|
759
|
+
parts.push(current);
|
|
760
|
+
}
|
|
761
|
+
return { type: 'TemplateLiteral', parts };
|
|
762
|
+
}
|
|
763
|
+
parseObjectLiteral() {
|
|
764
|
+
const properties = [];
|
|
765
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
766
|
+
// Spread property: ...expr or just ... (spread pipe context)
|
|
767
|
+
if (this.match(TokenType.SPREAD)) {
|
|
768
|
+
// In pipe context, bare ... spreads the pipe value
|
|
769
|
+
if (this.inPipeObjectContext &&
|
|
770
|
+
(this.check(TokenType.COMMA) || this.check(TokenType.RBRACE))) {
|
|
771
|
+
properties.push({ type: 'SpreadProperty', argument: { type: 'PipeContextRef' } });
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
const argument = this.parseExpression();
|
|
775
|
+
properties.push({ type: 'SpreadProperty', argument });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Computed property: [expr]: value
|
|
779
|
+
else if (this.match(TokenType.LBRACKET)) {
|
|
780
|
+
const key = this.parseExpression();
|
|
781
|
+
this.consume(TokenType.RBRACKET, 'Expected "]" after computed property key');
|
|
782
|
+
this.consume(TokenType.COLON, 'Expected ":" after computed property key');
|
|
783
|
+
const value = this.parseExpression();
|
|
784
|
+
properties.push({ type: 'ComputedProperty', key, value });
|
|
785
|
+
}
|
|
786
|
+
// String key or identifier
|
|
787
|
+
else {
|
|
788
|
+
let key;
|
|
789
|
+
if (this.match(TokenType.STRING)) {
|
|
790
|
+
key = this.previous().value;
|
|
791
|
+
}
|
|
792
|
+
else if (this.match(TokenType.IDENTIFIER)) {
|
|
793
|
+
key = this.previous().value;
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
throw new ParseError('Expected property name', this.peek());
|
|
797
|
+
}
|
|
798
|
+
// Shorthand: { foo } means { foo: foo } in normal context
|
|
799
|
+
// But in pipe context: { foo } means { foo: .foo } (reference pipe value)
|
|
800
|
+
if (!this.check(TokenType.COLON)) {
|
|
801
|
+
if (this.inPipeObjectContext) {
|
|
802
|
+
// In pipe context, expand shorthand to reference pipe value
|
|
803
|
+
properties.push({
|
|
804
|
+
type: 'StandardProperty',
|
|
805
|
+
key,
|
|
806
|
+
value: {
|
|
807
|
+
type: 'MemberAccess',
|
|
808
|
+
object: { type: 'PipeContextRef' },
|
|
809
|
+
property: key,
|
|
810
|
+
optional: false,
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
properties.push({ type: 'ShorthandProperty', key });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
this.consume(TokenType.COLON, 'Expected ":" after property key');
|
|
820
|
+
const value = this.parseExpression();
|
|
821
|
+
properties.push({ type: 'StandardProperty', key, value });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
825
|
+
this.consume(TokenType.COMMA, 'Expected "," between properties');
|
|
826
|
+
// Allow trailing comma
|
|
827
|
+
if (this.check(TokenType.RBRACE))
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
this.consume(TokenType.RBRACE, 'Expected "}" after object literal');
|
|
832
|
+
return { type: 'ObjectLiteral', properties };
|
|
833
|
+
}
|
|
834
|
+
parseArrayLiteral() {
|
|
835
|
+
const elements = [];
|
|
836
|
+
while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
|
|
837
|
+
if (this.match(TokenType.SPREAD)) {
|
|
838
|
+
const argument = this.parseExpression();
|
|
839
|
+
elements.push({ type: 'SpreadElement', argument });
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
elements.push(this.parseExpression());
|
|
843
|
+
}
|
|
844
|
+
if (!this.check(TokenType.RBRACKET)) {
|
|
845
|
+
this.consume(TokenType.COMMA, 'Expected "," between elements');
|
|
846
|
+
// Allow trailing comma
|
|
847
|
+
if (this.check(TokenType.RBRACKET))
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
this.consume(TokenType.RBRACKET, 'Expected "]" after array literal');
|
|
852
|
+
return { type: 'ArrayLiteral', elements };
|
|
853
|
+
}
|
|
854
|
+
parseIfExpression() {
|
|
855
|
+
const conditions = [];
|
|
856
|
+
// First condition
|
|
857
|
+
this.consume(TokenType.LPAREN, 'Expected "(" after "if"');
|
|
858
|
+
const firstTest = this.parseExpression();
|
|
859
|
+
this.consume(TokenType.RPAREN, 'Expected ")" after condition');
|
|
860
|
+
let firstConsequent;
|
|
861
|
+
if (this.match(TokenType.LBRACE)) {
|
|
862
|
+
firstConsequent = this.parseBlockExpression();
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
firstConsequent = this.parseExpression();
|
|
866
|
+
}
|
|
867
|
+
conditions.push({ type: 'ConditionalBranch', test: firstTest, consequent: firstConsequent });
|
|
868
|
+
// else if / else
|
|
869
|
+
let alternate = null;
|
|
870
|
+
while (this.match(TokenType.ELSE)) {
|
|
871
|
+
if (this.match(TokenType.IF)) {
|
|
872
|
+
// else if
|
|
873
|
+
this.consume(TokenType.LPAREN, 'Expected "(" after "else if"');
|
|
874
|
+
const test = this.parseExpression();
|
|
875
|
+
this.consume(TokenType.RPAREN, 'Expected ")" after condition');
|
|
876
|
+
let consequent;
|
|
877
|
+
if (this.match(TokenType.LBRACE)) {
|
|
878
|
+
consequent = this.parseBlockExpression();
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
consequent = this.parseExpression();
|
|
882
|
+
}
|
|
883
|
+
conditions.push({ type: 'ConditionalBranch', test, consequent });
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
// else
|
|
887
|
+
if (this.match(TokenType.LBRACE)) {
|
|
888
|
+
alternate = this.parseBlockExpression();
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
alternate = this.parseExpression();
|
|
892
|
+
}
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return { type: 'IfExpression', conditions, alternate };
|
|
897
|
+
}
|
|
898
|
+
parseBlockExpression() {
|
|
899
|
+
// For now, just parse a single expression in the block
|
|
900
|
+
const expr = this.parseExpression();
|
|
901
|
+
this.consume(TokenType.RBRACE, 'Expected "}" after block');
|
|
902
|
+
return expr;
|
|
903
|
+
}
|
|
904
|
+
parseParenOrArrow() {
|
|
905
|
+
// Could be: (expr), (), (a, b) => expr
|
|
906
|
+
// Empty parens - must be arrow function with no params
|
|
907
|
+
if (this.match(TokenType.RPAREN)) {
|
|
908
|
+
this.consume(TokenType.ARROW, 'Expected "=>" after "()"');
|
|
909
|
+
// No params, so no implicit property access available
|
|
910
|
+
const body = this.parseExpression();
|
|
911
|
+
return { type: 'ArrowFunction', params: [], body };
|
|
912
|
+
}
|
|
913
|
+
// Parse first expression
|
|
914
|
+
const first = this.parseExpression();
|
|
915
|
+
// Check for comma - indicates arrow function params
|
|
916
|
+
if (this.match(TokenType.COMMA)) {
|
|
917
|
+
const params = [this.exprToParam(first)];
|
|
918
|
+
do {
|
|
919
|
+
const param = this.parseExpression();
|
|
920
|
+
params.push(this.exprToParam(param));
|
|
921
|
+
} while (this.match(TokenType.COMMA));
|
|
922
|
+
this.consume(TokenType.RPAREN, 'Expected ")" after parameters');
|
|
923
|
+
this.consume(TokenType.ARROW, 'Expected "=>" after parameters');
|
|
924
|
+
// Push first param to stack so .property inside body resolves to param.property
|
|
925
|
+
this.arrowParamStack.push(params[0].name);
|
|
926
|
+
const body = this.parseExpression();
|
|
927
|
+
this.arrowParamStack.pop();
|
|
928
|
+
return { type: 'ArrowFunction', params, body };
|
|
929
|
+
}
|
|
930
|
+
this.consume(TokenType.RPAREN, 'Expected ")"');
|
|
931
|
+
// Check for arrow
|
|
932
|
+
if (this.match(TokenType.ARROW)) {
|
|
933
|
+
const params = [this.exprToParam(first)];
|
|
934
|
+
// Push param to stack so .property inside body resolves to param.property
|
|
935
|
+
this.arrowParamStack.push(params[0].name);
|
|
936
|
+
const body = this.parseExpression();
|
|
937
|
+
this.arrowParamStack.pop();
|
|
938
|
+
return { type: 'ArrowFunction', params, body };
|
|
939
|
+
}
|
|
940
|
+
// Just a parenthesized expression
|
|
941
|
+
return first;
|
|
942
|
+
}
|
|
943
|
+
exprToParam(expr) {
|
|
944
|
+
if (expr.type === 'Identifier') {
|
|
945
|
+
return { type: 'Parameter', name: expr.name };
|
|
946
|
+
}
|
|
947
|
+
throw new ParseError('Expected parameter name', this.peek());
|
|
948
|
+
}
|
|
949
|
+
// ===========================================================================
|
|
950
|
+
// TYPE ANNOTATIONS
|
|
951
|
+
// ===========================================================================
|
|
952
|
+
parseTypeAnnotation() {
|
|
953
|
+
const type = this.parsePrimaryType();
|
|
954
|
+
// Union type: string | number
|
|
955
|
+
if (this.check(TokenType.PIPE)) {
|
|
956
|
+
const types = [type];
|
|
957
|
+
while (this.match(TokenType.PIPE)) {
|
|
958
|
+
types.push(this.parsePrimaryType());
|
|
959
|
+
}
|
|
960
|
+
return { type: 'UnionType', types };
|
|
961
|
+
}
|
|
962
|
+
return type;
|
|
963
|
+
}
|
|
964
|
+
parsePrimaryType() {
|
|
965
|
+
if (this.match(TokenType.IDENTIFIER)) {
|
|
966
|
+
const name = this.previous().value;
|
|
967
|
+
// Check for primitive types
|
|
968
|
+
if (['string', 'number', 'boolean', 'null', 'any'].includes(name)) {
|
|
969
|
+
const nonNull = this.match(TokenType.BANG);
|
|
970
|
+
return { type: 'PrimitiveType', name: name, nonNull };
|
|
971
|
+
}
|
|
972
|
+
// Array<T>
|
|
973
|
+
if (name === 'Array' && this.match(TokenType.LT)) {
|
|
974
|
+
const elementType = this.parseTypeAnnotation();
|
|
975
|
+
this.consume(TokenType.GT, 'Expected ">" after array element type');
|
|
976
|
+
return { type: 'ArrayType', elementType };
|
|
977
|
+
}
|
|
978
|
+
// Type reference
|
|
979
|
+
return { type: 'TypeReference', name };
|
|
980
|
+
}
|
|
981
|
+
// Object type: { key: type }
|
|
982
|
+
if (this.match(TokenType.LBRACE)) {
|
|
983
|
+
const properties = [];
|
|
984
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
985
|
+
const key = this.consume(TokenType.IDENTIFIER, 'Expected property name');
|
|
986
|
+
const optional = this.match(TokenType.QUESTION);
|
|
987
|
+
this.consume(TokenType.COLON, 'Expected ":" after property name');
|
|
988
|
+
const valueType = this.parseTypeAnnotation();
|
|
989
|
+
properties.push({ type: 'ObjectTypeProperty', key: key.value, valueType, optional });
|
|
990
|
+
if (!this.check(TokenType.RBRACE)) {
|
|
991
|
+
this.consume(TokenType.COMMA, 'Expected "," between type properties');
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
this.consume(TokenType.RBRACE, 'Expected "}" after object type');
|
|
995
|
+
return { type: 'ObjectType', properties };
|
|
996
|
+
}
|
|
997
|
+
throw new ParseError('Expected type annotation', this.peek());
|
|
998
|
+
}
|
|
999
|
+
// ===========================================================================
|
|
1000
|
+
// HELPER METHODS
|
|
1001
|
+
// ===========================================================================
|
|
1002
|
+
isAtEnd() {
|
|
1003
|
+
return this.peek().type === TokenType.EOF;
|
|
1004
|
+
}
|
|
1005
|
+
peek() {
|
|
1006
|
+
return this.tokens[this.current];
|
|
1007
|
+
}
|
|
1008
|
+
peekNext() {
|
|
1009
|
+
if (this.current + 1 >= this.tokens.length) {
|
|
1010
|
+
return this.tokens[this.tokens.length - 1]; // Return EOF
|
|
1011
|
+
}
|
|
1012
|
+
return this.tokens[this.current + 1];
|
|
1013
|
+
}
|
|
1014
|
+
previous() {
|
|
1015
|
+
return this.tokens[this.current - 1];
|
|
1016
|
+
}
|
|
1017
|
+
advance() {
|
|
1018
|
+
if (!this.isAtEnd())
|
|
1019
|
+
this.current++;
|
|
1020
|
+
return this.previous();
|
|
1021
|
+
}
|
|
1022
|
+
check(type) {
|
|
1023
|
+
if (this.isAtEnd())
|
|
1024
|
+
return false;
|
|
1025
|
+
return this.peek().type === type;
|
|
1026
|
+
}
|
|
1027
|
+
match(...types) {
|
|
1028
|
+
for (const type of types) {
|
|
1029
|
+
if (this.check(type)) {
|
|
1030
|
+
this.advance();
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
consume(type, message) {
|
|
1037
|
+
if (this.check(type))
|
|
1038
|
+
return this.advance();
|
|
1039
|
+
throw new ParseError(message, this.peek(), TokenType[type]);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
export function parse(input) {
|
|
1043
|
+
return new Parser(input).parse();
|
|
1044
|
+
}
|
|
1045
|
+
//# sourceMappingURL=parser.js.map
|