naidejs 1.0.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/src/parser.js ADDED
@@ -0,0 +1,1079 @@
1
+ import { T, TYPE_TOKENS } from './tokens.js';
2
+
3
+ class ASTNode {
4
+ constructor(type, props = {}) {
5
+ this.type = type;
6
+ Object.assign(this, props);
7
+ }
8
+ }
9
+
10
+ export class Parser {
11
+ constructor(tokens) {
12
+ this.tokens = this.preprocessTokens(tokens.filter(t => t.type !== T.COMMENT));
13
+ this.pos = 0;
14
+ }
15
+
16
+ preprocessTokens(tokens) {
17
+ // Remove INDENT/DEDENT pairs around pipe continuation lines
18
+ // Pattern: NEWLINE INDENT PIPE -> NEWLINE PIPE (and remove matching DEDENT)
19
+ const result = [...tokens];
20
+ const indentsToRemove = new Set();
21
+
22
+ for (let i = 0; i < result.length; i++) {
23
+ if (result[i].type !== T.INDENT) continue;
24
+ // Look forward past any newlines for PIPE
25
+ let j = i + 1;
26
+ while (j < result.length && result[j].type === T.NEWLINE) j++;
27
+ if (j < result.length && result[j].type === T.PIPE) {
28
+ indentsToRemove.add(i);
29
+ }
30
+ }
31
+
32
+ if (indentsToRemove.size === 0) return result;
33
+
34
+ // For each INDENT to remove, find matching DEDENT
35
+ const dedentsToRemove = new Set();
36
+ for (const idx of indentsToRemove) {
37
+ let depth = 0;
38
+ for (let i = idx; i < result.length; i++) {
39
+ if (result[i].type === T.INDENT) depth++;
40
+ if (result[i].type === T.DEDENT) {
41
+ depth--;
42
+ if (depth === 0) {
43
+ dedentsToRemove.add(i);
44
+ break;
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ const allRemove = new Set([...indentsToRemove, ...dedentsToRemove]);
51
+ return result.filter((_, i) => !allRemove.has(i));
52
+ }
53
+
54
+ peek(offset = 0) {
55
+ const idx = this.pos + offset;
56
+ return idx < this.tokens.length ? this.tokens[idx] : { type: T.EOF, value: '' };
57
+ }
58
+
59
+ advance() {
60
+ const tok = this.tokens[this.pos];
61
+ this.pos++;
62
+ return tok;
63
+ }
64
+
65
+ expect(type) {
66
+ const tok = this.advance();
67
+ if (tok.type !== type) {
68
+ throw this.error(`Expected ${type} but got ${tok.type} ('${tok.value}')`, tok);
69
+ }
70
+ return tok;
71
+ }
72
+
73
+ match(type) {
74
+ if (this.peek().type === type) {
75
+ return this.advance();
76
+ }
77
+ return null;
78
+ }
79
+
80
+ at(type) {
81
+ return this.peek().type === type;
82
+ }
83
+
84
+ atAny(...types) {
85
+ return types.includes(this.peek().type);
86
+ }
87
+
88
+ expectPropertyName() {
89
+ const tok = this.advance();
90
+ if (tok.type === T.IDENT || TYPE_TOKENS.has(tok.type) ||
91
+ tok.type === T.LOG || tok.type === T.DB || tok.type === T.GET ||
92
+ tok.type === T.POST || tok.type === T.PUT || tok.type === T.DEL ||
93
+ tok.type === T.MATCH || tok.type === T.ON || tok.type === T.NEW ||
94
+ tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF) {
95
+ return tok.value;
96
+ }
97
+ throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
98
+ }
99
+
100
+ error(msg, tok) {
101
+ const t = tok || this.peek();
102
+ return new Error(`[NAIDE Parse Error] ${msg} at line ${t.line}:${t.col}`);
103
+ }
104
+
105
+ skipNewlines() {
106
+ while (this.at(T.NEWLINE)) this.advance();
107
+ }
108
+
109
+ skipWhitespace() {
110
+ while (this.atAny(T.NEWLINE, T.INDENT, T.DEDENT)) this.advance();
111
+ }
112
+
113
+ parse() {
114
+ const body = [];
115
+ this.skipNewlines();
116
+ while (!this.at(T.EOF)) {
117
+ const stmt = this.parseStatement();
118
+ if (stmt) body.push(stmt);
119
+ this.skipNewlines();
120
+ }
121
+ return new ASTNode('Program', { body });
122
+ }
123
+
124
+ parseStatement() {
125
+ this.skipNewlines();
126
+ const tok = this.peek();
127
+
128
+ switch (tok.type) {
129
+ case T.USE: return this.parseUse();
130
+ case T.FN: return this.parseFunction(false, false);
131
+ case T.FN_ASYNC: return this.parseFunction(true, false);
132
+ case T.PUB: return this.parsePub();
133
+ case T.RET: return this.parseReturn();
134
+ case T.IF: return this.parseIf();
135
+ case T.EACH: return this.parseEach();
136
+ case T.FOR: return this.parseFor();
137
+ case T.WHILE: return this.parseWhile();
138
+ case T.MATCH: return this.parseMatch();
139
+ case T.TRY: return this.parseTry();
140
+ case T.SERVER: return this.parseServer();
141
+ case T.MODEL: return this.parseModel();
142
+ case T.ON: return this.parseOn();
143
+ case T.LOG: return this.parseLog();
144
+ case T.THROW: return this.parseThrow();
145
+ case T.BREAK: this.advance(); return new ASTNode('Break');
146
+ case T.CONTINUE: this.advance(); return new ASTNode('Continue');
147
+ case T.MUT: return this.parseMutVariable();
148
+ case T.DB: return this.parseDbStatement();
149
+ case T.AWAIT: return this.parseAwaitStatement();
150
+ case T.AWAIT_ALL: return this.parseAwaitAll();
151
+ default:
152
+ if (TYPE_TOKENS.has(tok.type)) {
153
+ return this.parseTypedVariable();
154
+ }
155
+ return this.parseExpressionStatement();
156
+ }
157
+ }
158
+
159
+ // use express
160
+ // use express from "express"
161
+ // use {readFile, writeFile} from "fs/promises"
162
+ parseUse() {
163
+ this.advance(); // use
164
+
165
+ if (this.at(T.LBRACE)) {
166
+ // use {a, b} from "module"
167
+ this.advance();
168
+ const names = [];
169
+ while (!this.at(T.RBRACE) && !this.at(T.EOF)) {
170
+ const name = this.expect(T.IDENT).value;
171
+ let alias = null;
172
+ if (this.match(T.AS)) {
173
+ alias = this.expect(T.IDENT).value;
174
+ }
175
+ names.push({ name, alias });
176
+ this.match(T.COMMA);
177
+ }
178
+ this.expect(T.RBRACE);
179
+ this.expect(T.FROM);
180
+ const source = this.parseString();
181
+ return new ASTNode('UseDestructured', { names, source });
182
+ }
183
+
184
+ const name = this.expect(T.IDENT).value;
185
+ let source = null;
186
+ let alias = null;
187
+
188
+ if (this.match(T.AS)) {
189
+ alias = this.expect(T.IDENT).value;
190
+ }
191
+
192
+ if (this.match(T.FROM)) {
193
+ source = this.parseString();
194
+ }
195
+
196
+ return new ASTNode('Use', { name, source, alias });
197
+ }
198
+
199
+ parseString() {
200
+ const tok = this.expect(T.STRING);
201
+ return tok.value;
202
+ }
203
+
204
+ // fn name(params) -> returnType:
205
+ // body
206
+ parseFunction(isAsync, isPublic) {
207
+ this.advance(); // fn or fn.async
208
+ const name = this.expect(T.IDENT).value;
209
+ this.expect(T.LPAREN);
210
+ const params = this.parseFnParams();
211
+ this.expect(T.RPAREN);
212
+
213
+ let returnType = null;
214
+ if (this.match(T.ARROW)) {
215
+ returnType = this.parseTypeAnnotation();
216
+ }
217
+
218
+ this.expect(T.COLON);
219
+ const body = this.parseBlock();
220
+
221
+ return new ASTNode('Function', { name, params, returnType, body, isAsync, isPublic });
222
+ }
223
+
224
+ parseFnParams() {
225
+ const params = [];
226
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
227
+ let type = null;
228
+ let spread = false;
229
+
230
+ if (this.match(T.SPREAD)) {
231
+ spread = true;
232
+ }
233
+
234
+ if (TYPE_TOKENS.has(this.peek().type)) {
235
+ type = this.advance().value;
236
+ }
237
+
238
+ const name = this.expect(T.IDENT).value;
239
+
240
+ let defaultValue = null;
241
+ if (this.match(T.ASSIGN)) {
242
+ defaultValue = this.parseExpression();
243
+ }
244
+
245
+ params.push({ name, type, defaultValue, spread });
246
+ this.match(T.COMMA);
247
+ }
248
+ return params;
249
+ }
250
+
251
+ parseTypeAnnotation() {
252
+ if (TYPE_TOKENS.has(this.peek().type)) {
253
+ return this.advance().value;
254
+ }
255
+ return this.expect(T.IDENT).value;
256
+ }
257
+
258
+ parseBlock() {
259
+ this.skipNewlines();
260
+ this.expect(T.INDENT);
261
+ const body = [];
262
+ this.skipNewlines();
263
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
264
+ const stmt = this.parseStatement();
265
+ if (stmt) body.push(stmt);
266
+ this.skipNewlines();
267
+ }
268
+ this.match(T.DEDENT);
269
+ return body;
270
+ }
271
+
272
+ parsePub() {
273
+ this.advance(); // pub
274
+ if (this.at(T.FN)) return this.parseFunction(false, true);
275
+ if (this.at(T.FN_ASYNC)) return this.parseFunction(true, true);
276
+ if (this.at(T.MODEL)) return this.parseModel(true);
277
+
278
+ // pub variable
279
+ if (TYPE_TOKENS.has(this.peek().type)) {
280
+ const node = this.parseTypedVariable();
281
+ node.isPublic = true;
282
+ return node;
283
+ }
284
+
285
+ throw this.error('Expected fn, model, or type after pub');
286
+ }
287
+
288
+ // ret expression
289
+ parseReturn() {
290
+ this.advance(); // ret
291
+ if (this.at(T.NEWLINE) || this.at(T.EOF) || this.at(T.DEDENT)) {
292
+ return new ASTNode('Return', { value: null });
293
+ }
294
+ // ret.status 200 {...}
295
+ if (this.match(T.DOT)) {
296
+ const method = this.expect(T.IDENT).value;
297
+ const statusCode = this.parseExpression();
298
+ const body = this.parseExpression();
299
+ return new ASTNode('ReturnStatus', { method, statusCode, body });
300
+ }
301
+ const value = this.parseExpression();
302
+ return new ASTNode('Return', { value });
303
+ }
304
+
305
+ // str name = "hello"
306
+ parseTypedVariable() {
307
+ const varType = this.advance().value;
308
+ const name = this.expect(T.IDENT).value;
309
+ this.expect(T.ASSIGN);
310
+ const value = this.parseExpression();
311
+ return new ASTNode('TypedVar', { varType, name, value, isMut: false, isPublic: false });
312
+ }
313
+
314
+ // mut int counter = 0
315
+ parseMutVariable() {
316
+ this.advance(); // mut
317
+ if (TYPE_TOKENS.has(this.peek().type)) {
318
+ const node = this.parseTypedVariable();
319
+ node.isMut = true;
320
+ return node;
321
+ }
322
+ // mut name = value (no type)
323
+ const name = this.expect(T.IDENT).value;
324
+ this.expect(T.ASSIGN);
325
+ const value = this.parseExpression();
326
+ return new ASTNode('TypedVar', { varType: null, name, value, isMut: true });
327
+ }
328
+
329
+ // if condition:
330
+ // body
331
+ // elif condition:
332
+ // body
333
+ // else:
334
+ // body
335
+ parseIf() {
336
+ this.advance(); // if
337
+ const condition = this.parseExpression();
338
+ this.expect(T.COLON);
339
+ const body = this.parseBlock();
340
+
341
+ const elifs = [];
342
+ this.skipNewlines();
343
+ while (this.at(T.ELIF)) {
344
+ this.advance();
345
+ const elifCond = this.parseExpression();
346
+ this.expect(T.COLON);
347
+ const elifBody = this.parseBlock();
348
+ elifs.push({ condition: elifCond, body: elifBody });
349
+ this.skipNewlines();
350
+ }
351
+
352
+ let elseBody = null;
353
+ if (this.at(T.ELSE)) {
354
+ this.advance();
355
+ this.expect(T.COLON);
356
+ elseBody = this.parseBlock();
357
+ }
358
+
359
+ return new ASTNode('If', { condition, body, elifs, elseBody });
360
+ }
361
+
362
+ // each item in collection:
363
+ // body
364
+ parseEach() {
365
+ this.advance(); // each
366
+ let key = null;
367
+ const valueName = this.expect(T.IDENT).value;
368
+ if (this.match(T.COMMA)) {
369
+ key = valueName;
370
+ // The next ident is actually the value
371
+ const val = this.expect(T.IDENT).value;
372
+ this.expect(T.IN);
373
+ const collection = this.parseExpression();
374
+ this.expect(T.COLON);
375
+ const body = this.parseBlock();
376
+ return new ASTNode('Each', { key, value: val, collection, body });
377
+ }
378
+ this.expect(T.IN);
379
+ const collection = this.parseExpression();
380
+ this.expect(T.COLON);
381
+ const body = this.parseBlock();
382
+ return new ASTNode('Each', { key: null, value: valueName, collection, body });
383
+ }
384
+
385
+ // for i in 0..10:
386
+ // body
387
+ parseFor() {
388
+ this.advance(); // for
389
+ const varName = this.expect(T.IDENT).value;
390
+ this.expect(T.IN);
391
+ const start = this.parseExpression();
392
+ this.expect(T.RANGE);
393
+ const end = this.parseExpression();
394
+ this.expect(T.COLON);
395
+ const body = this.parseBlock();
396
+ return new ASTNode('For', { varName, start, end, body });
397
+ }
398
+
399
+ // while condition:
400
+ // body
401
+ parseWhile() {
402
+ this.advance(); // while
403
+ const condition = this.parseExpression();
404
+ this.expect(T.COLON);
405
+ const body = this.parseBlock();
406
+ return new ASTNode('While', { condition, body });
407
+ }
408
+
409
+ // match value:
410
+ // pattern: body
411
+ // _: default
412
+ parseMatch() {
413
+ this.advance(); // match
414
+ const value = this.parseExpression();
415
+ this.expect(T.COLON);
416
+ this.skipNewlines();
417
+ this.expect(T.INDENT);
418
+
419
+ const cases = [];
420
+ this.skipNewlines();
421
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
422
+ let pattern;
423
+ if (this.at(T.IDENT) && this.peek().value === '_') {
424
+ this.advance();
425
+ pattern = new ASTNode('DefaultPattern');
426
+ } else {
427
+ pattern = this.parseExpression();
428
+ }
429
+ this.expect(T.COLON);
430
+
431
+ let body;
432
+ if (this.at(T.NEWLINE) || this.at(T.INDENT)) {
433
+ this.skipNewlines();
434
+ if (this.at(T.INDENT)) {
435
+ body = this.parseBlock();
436
+ } else {
437
+ body = [this.parseStatement()];
438
+ }
439
+ } else {
440
+ body = [this.parseStatement()];
441
+ }
442
+ cases.push({ pattern, body });
443
+ this.skipNewlines();
444
+ }
445
+ this.match(T.DEDENT);
446
+ return new ASTNode('Match', { value, cases });
447
+ }
448
+
449
+ // try:
450
+ // body
451
+ // fail e:
452
+ // handler
453
+ parseTry() {
454
+ this.advance(); // try
455
+ this.expect(T.COLON);
456
+ const body = this.parseBlock();
457
+
458
+ this.skipNewlines();
459
+ let catchVar = null;
460
+ let catchBody = null;
461
+ if (this.at(T.FAIL)) {
462
+ this.advance();
463
+ if (!this.at(T.COLON)) {
464
+ catchVar = this.expect(T.IDENT).value;
465
+ }
466
+ this.expect(T.COLON);
467
+ catchBody = this.parseBlock();
468
+ }
469
+
470
+ return new ASTNode('Try', { body, catchVar, catchBody });
471
+ }
472
+
473
+ // server app port 3000:
474
+ // get "/path" -> type:
475
+ // body
476
+ parseServer() {
477
+ this.advance(); // server
478
+ const name = this.expect(T.IDENT).value;
479
+
480
+ let portKw = null;
481
+ let port = null;
482
+ if (this.at(T.IDENT) && this.peek().value === 'port') {
483
+ this.advance();
484
+ port = this.parseExpression();
485
+ }
486
+
487
+ this.expect(T.COLON);
488
+ this.skipNewlines();
489
+ this.expect(T.INDENT);
490
+
491
+ const routes = [];
492
+ const middleware = [];
493
+ this.skipNewlines();
494
+
495
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
496
+ if (this.atAny(T.GET, T.POST, T.PUT, T.DEL)) {
497
+ routes.push(this.parseRoute());
498
+ } else if (this.at(T.MID)) {
499
+ middleware.push(this.parseMiddleware());
500
+ } else {
501
+ // generic statement in server block
502
+ routes.push(this.parseStatement());
503
+ }
504
+ this.skipNewlines();
505
+ }
506
+ this.match(T.DEDENT);
507
+
508
+ return new ASTNode('Server', { name, port, routes, middleware });
509
+ }
510
+
511
+ parseRoute() {
512
+ const method = this.advance().value; // get/post/put/del
513
+ const path = this.parseString();
514
+
515
+ let params = [];
516
+ if (this.match(T.LPAREN)) {
517
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
518
+ params.push(this.expect(T.IDENT).value);
519
+ this.match(T.COMMA);
520
+ }
521
+ this.expect(T.RPAREN);
522
+ }
523
+
524
+ let returnType = null;
525
+ if (this.match(T.ARROW)) {
526
+ returnType = this.parseTypeAnnotation();
527
+ }
528
+
529
+ this.expect(T.COLON);
530
+ const body = this.parseBlock();
531
+
532
+ return new ASTNode('Route', { method, path, params, returnType, body });
533
+ }
534
+
535
+ parseMiddleware() {
536
+ this.advance(); // mid
537
+ const name = this.expect(T.IDENT).value;
538
+ this.expect(T.LPAREN);
539
+ const params = [];
540
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
541
+ params.push(this.expect(T.IDENT).value);
542
+ this.match(T.COMMA);
543
+ }
544
+ this.expect(T.RPAREN);
545
+ this.expect(T.COLON);
546
+ const body = this.parseBlock();
547
+ return new ASTNode('Middleware', { name, params, body });
548
+ }
549
+
550
+ // model User:
551
+ // str name
552
+ // int age
553
+ // fn greet() -> str:
554
+ // ret "hi"
555
+ parseModel(isPublic = false) {
556
+ this.advance(); // model
557
+ const name = this.expect(T.IDENT).value;
558
+
559
+ let parent = null;
560
+ if (this.at(T.IDENT) && this.peek().value === 'extends') {
561
+ this.advance();
562
+ parent = this.expect(T.IDENT).value;
563
+ }
564
+
565
+ this.expect(T.COLON);
566
+ this.skipNewlines();
567
+ this.expect(T.INDENT);
568
+
569
+ const fields = [];
570
+ const methods = [];
571
+ this.skipNewlines();
572
+
573
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
574
+ if (this.at(T.FN) || this.at(T.FN_ASYNC)) {
575
+ const isAsync = this.at(T.FN_ASYNC);
576
+ methods.push(this.parseFunction(isAsync, false));
577
+ } else if (TYPE_TOKENS.has(this.peek().type)) {
578
+ const fieldType = this.advance().value;
579
+ const fieldName = this.expect(T.IDENT).value;
580
+ let defaultValue = null;
581
+ if (this.match(T.ASSIGN)) {
582
+ defaultValue = this.parseExpression();
583
+ }
584
+ fields.push({ name: fieldName, type: fieldType, defaultValue });
585
+ } else {
586
+ // skip unknown
587
+ this.advance();
588
+ }
589
+ this.skipNewlines();
590
+ }
591
+ this.match(T.DEDENT);
592
+
593
+ return new ASTNode('Model', { name, parent, fields, methods, isPublic });
594
+ }
595
+
596
+ // on event:
597
+ // body
598
+ parseOn() {
599
+ this.advance(); // on
600
+ const event = this.parseExpression();
601
+ this.expect(T.COLON);
602
+ const body = this.parseBlock();
603
+ return new ASTNode('On', { event, body });
604
+ }
605
+
606
+ // log "message"
607
+ // log.error "message"
608
+ parseLog() {
609
+ this.advance(); // log
610
+ let level = 'log';
611
+ if (this.match(T.DOT)) {
612
+ level = this.expect(T.IDENT).value;
613
+ }
614
+ const args = [this.parseExpression()];
615
+ while (this.match(T.COMMA)) {
616
+ args.push(this.parseExpression());
617
+ }
618
+ return new ASTNode('Log', { level, args });
619
+ }
620
+
621
+ parseThrow() {
622
+ this.advance(); // throw
623
+ const value = this.parseExpression();
624
+ return new ASTNode('Throw', { value });
625
+ }
626
+
627
+ parseDbStatement() {
628
+ this.advance(); // db
629
+ this.expect(T.DOT);
630
+ const method = this.expect(T.IDENT).value;
631
+ if (method === 'connect') {
632
+ const connectionString = this.parseExpression();
633
+ return new ASTNode('DbConnect', { connectionString });
634
+ }
635
+ // db.query, db.find, etc -> treat as expression
636
+ this.pos -= 3; // rewind to parse as expression
637
+ return this.parseExpressionStatement();
638
+ }
639
+
640
+ parseAwaitStatement() {
641
+ // could be await expression or standalone await call
642
+ return this.parseExpressionStatement();
643
+ }
644
+
645
+ // [a, b] = await.all [expr1, expr2]
646
+ parseAwaitAll() {
647
+ this.advance(); // await.all
648
+ const exprs = [];
649
+ if (this.match(T.LBRACKET)) {
650
+ while (!this.at(T.RBRACKET) && !this.at(T.EOF)) {
651
+ this.skipWhitespace();
652
+ if (this.at(T.RBRACKET)) break;
653
+ exprs.push(this.parseExpression());
654
+ this.match(T.COMMA);
655
+ this.skipWhitespace();
656
+ }
657
+ this.expect(T.RBRACKET);
658
+ } else {
659
+ exprs.push(this.parseExpression());
660
+ }
661
+ return new ASTNode('AwaitAll', { expressions: exprs });
662
+ }
663
+
664
+ parseExpressionStatement() {
665
+ const expr = this.parseExpression();
666
+
667
+ // Check for assignment: expr = value
668
+ if (this.match(T.ASSIGN)) {
669
+ const value = this.parseExpression();
670
+ return new ASTNode('Assignment', { target: expr, value });
671
+ }
672
+ if (this.match(T.PLUS_ASSIGN)) {
673
+ const value = this.parseExpression();
674
+ return new ASTNode('CompoundAssign', { target: expr, op: '+=', value });
675
+ }
676
+ if (this.match(T.MINUS_ASSIGN)) {
677
+ const value = this.parseExpression();
678
+ return new ASTNode('CompoundAssign', { target: expr, op: '-=', value });
679
+ }
680
+
681
+ return new ASTNode('ExprStatement', { expression: expr });
682
+ }
683
+
684
+ // Expression parsing with precedence climbing
685
+ parseExpression() {
686
+ return this.parsePipe();
687
+ }
688
+
689
+ parsePipe() {
690
+ let left = this.parseTernary();
691
+ while (this.matchPipeAcrossLines()) {
692
+ this.skipWhitespace();
693
+ const right = this.parseTernary();
694
+ left = new ASTNode('Pipe', { left, right });
695
+ }
696
+ return left;
697
+ }
698
+
699
+ matchPipeAcrossLines() {
700
+ if (this.match(T.PIPE)) return true;
701
+ // Lookahead across newlines for pipe (INDENT/DEDENT already preprocessed)
702
+ let scanPos = this.pos;
703
+ while (scanPos < this.tokens.length && this.tokens[scanPos].type === T.NEWLINE) {
704
+ scanPos++;
705
+ }
706
+ if (scanPos < this.tokens.length && this.tokens[scanPos].type === T.PIPE) {
707
+ while (this.at(T.NEWLINE)) this.advance();
708
+ this.advance(); // consume PIPE
709
+ return true;
710
+ }
711
+ return false;
712
+ }
713
+
714
+ parseTernary() {
715
+ let expr = this.parseNullish();
716
+ // inline if: value = if cond then a else b
717
+ if (this.at(T.IF)) {
718
+ // Only treat as ternary if we're in an expression context
719
+ // Lookahead: if ... then ... else
720
+ const savedPos = this.pos;
721
+ this.advance(); // if
722
+ const condition = this.parseNullish();
723
+ if (this.match(T.THEN)) {
724
+ const consequent = this.parseNullish();
725
+ this.expect(T.ELSE);
726
+ const alternate = this.parseNullish();
727
+ return new ASTNode('Ternary', { condition, consequent, alternate });
728
+ }
729
+ // Not a ternary, restore
730
+ this.pos = savedPos;
731
+ }
732
+ return expr;
733
+ }
734
+
735
+ parseNullish() {
736
+ let left = this.parseOr();
737
+ while (this.match(T.NULLISH)) {
738
+ const right = this.parseOr();
739
+ left = new ASTNode('Binary', { op: '??', left, right });
740
+ }
741
+ return left;
742
+ }
743
+
744
+ parseOr() {
745
+ let left = this.parseAnd();
746
+ while (this.match(T.OR)) {
747
+ const right = this.parseAnd();
748
+ left = new ASTNode('Binary', { op: '||', left, right });
749
+ }
750
+ return left;
751
+ }
752
+
753
+ parseAnd() {
754
+ let left = this.parseComparison();
755
+ while (this.match(T.AND)) {
756
+ const right = this.parseComparison();
757
+ left = new ASTNode('Binary', { op: '&&', left, right });
758
+ }
759
+ return left;
760
+ }
761
+
762
+ parseComparison() {
763
+ let left = this.parseAddition();
764
+ while (this.atAny(T.EQ, T.NEQ, T.GT, T.LT, T.GTE, T.LTE)) {
765
+ const op = this.advance().value;
766
+ const right = this.parseAddition();
767
+ left = new ASTNode('Binary', { op: op === '==' ? '===' : op === '!=' ? '!==' : op, left, right });
768
+ }
769
+ return left;
770
+ }
771
+
772
+ parseAddition() {
773
+ let left = this.parseMultiplication();
774
+ while (this.atAny(T.PLUS, T.MINUS)) {
775
+ const op = this.advance().value;
776
+ const right = this.parseMultiplication();
777
+ left = new ASTNode('Binary', { op, left, right });
778
+ }
779
+ return left;
780
+ }
781
+
782
+ parseMultiplication() {
783
+ let left = this.parseUnary();
784
+ while (this.atAny(T.STAR, T.SLASH, T.PERCENT)) {
785
+ const op = this.advance().value;
786
+ const right = this.parseUnary();
787
+ left = new ASTNode('Binary', { op, left, right });
788
+ }
789
+ return left;
790
+ }
791
+
792
+ parseUnary() {
793
+ if (this.match(T.NOT)) {
794
+ const expr = this.parseUnary();
795
+ return new ASTNode('Unary', { op: '!', expr });
796
+ }
797
+ if (this.at(T.MINUS)) {
798
+ this.advance();
799
+ const expr = this.parseUnary();
800
+ return new ASTNode('Unary', { op: '-', expr });
801
+ }
802
+ if (this.match(T.AWAIT)) {
803
+ const expr = this.parseUnary();
804
+ return new ASTNode('Await', { expr });
805
+ }
806
+ if (this.at(T.AWAIT_ALL)) {
807
+ this.advance();
808
+ const expr = this.parsePostfix();
809
+ return new ASTNode('AwaitAllExpr', { expr });
810
+ }
811
+ if (this.match(T.SPREAD)) {
812
+ const expr = this.parseUnary();
813
+ return new ASTNode('Spread', { expr });
814
+ }
815
+ if (this.match(T.NEW)) {
816
+ const expr = this.parsePostfix();
817
+ return new ASTNode('New', { expr });
818
+ }
819
+ return this.parsePostfix();
820
+ }
821
+
822
+ parsePostfix() {
823
+ let expr = this.parsePrimary();
824
+
825
+ while (true) {
826
+ if (this.match(T.DOT)) {
827
+ const prop = this.expectPropertyName();
828
+ expr = new ASTNode('MemberAccess', { object: expr, property: prop });
829
+ } else if (this.match(T.OPTIONAL)) {
830
+ const prop = this.expectPropertyName();
831
+ expr = new ASTNode('OptionalAccess', { object: expr, property: prop });
832
+ } else if (this.at(T.LPAREN)) {
833
+ this.advance();
834
+ const args = this.parseArgList();
835
+ this.expect(T.RPAREN);
836
+ expr = new ASTNode('Call', { callee: expr, args });
837
+ } else if (this.at(T.LBRACKET)) {
838
+ this.advance();
839
+ const index = this.parseExpression();
840
+ this.expect(T.RBRACKET);
841
+ expr = new ASTNode('IndexAccess', { object: expr, index });
842
+ } else {
843
+ break;
844
+ }
845
+ }
846
+
847
+ return expr;
848
+ }
849
+
850
+ parseArgList() {
851
+ const args = [];
852
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
853
+ this.skipWhitespace();
854
+ if (this.at(T.RPAREN)) break;
855
+ args.push(this.parseExpression());
856
+ this.match(T.COMMA);
857
+ this.skipWhitespace();
858
+ }
859
+ return args;
860
+ }
861
+
862
+ parsePrimary() {
863
+ const tok = this.peek();
864
+
865
+ switch (tok.type) {
866
+ case T.NUMBER:
867
+ this.advance();
868
+ return new ASTNode('Number', { value: tok.value });
869
+
870
+ case T.STRING:
871
+ this.advance();
872
+ return new ASTNode('String', { value: tok.value });
873
+
874
+ case T.BOOL:
875
+ this.advance();
876
+ return new ASTNode('Bool', { value: tok.value === 'true' });
877
+
878
+ case T.NULL:
879
+ this.advance();
880
+ return new ASTNode('Null');
881
+
882
+ case T.SELF:
883
+ this.advance();
884
+ return new ASTNode('Self');
885
+
886
+ case T.IDENT:
887
+ this.advance();
888
+ return new ASTNode('Identifier', { name: tok.value });
889
+
890
+ case T.TYPE_STR: case T.TYPE_INT: case T.TYPE_NUM:
891
+ case T.TYPE_BOOL: case T.TYPE_LIST: case T.TYPE_MAP:
892
+ case T.TYPE_ANY: case T.TYPE_JSON: case T.TYPE_VOID:
893
+ // In expression context, type keywords act as identifiers (e.g., arr.map(), JSON.parse())
894
+ this.advance();
895
+ return new ASTNode('Identifier', { name: tok.value });
896
+
897
+ case T.DB:
898
+ return this.parseDbExpression();
899
+
900
+ case T.LOG:
901
+ return this.parseLogExpression();
902
+
903
+ case T.LPAREN:
904
+ return this.parseGroupOrArrow();
905
+
906
+ case T.LBRACKET:
907
+ return this.parseArray();
908
+
909
+ case T.LBRACE:
910
+ return this.parseObject();
911
+
912
+ case T.FN:
913
+ case T.FN_ASYNC:
914
+ return this.parseLambda();
915
+
916
+ default:
917
+ throw this.error(`Unexpected token ${tok.type} ('${tok.value}')`);
918
+ }
919
+ }
920
+
921
+ parseDbExpression() {
922
+ this.advance(); // db
923
+ this.expect(T.DOT);
924
+ const method = this.expect(T.IDENT).value;
925
+ if (this.at(T.LPAREN)) {
926
+ this.advance();
927
+ const args = this.parseArgList();
928
+ this.expect(T.RPAREN);
929
+ return new ASTNode('Call', {
930
+ callee: new ASTNode('MemberAccess', {
931
+ object: new ASTNode('Identifier', { name: 'db' }),
932
+ property: method
933
+ }),
934
+ args
935
+ });
936
+ }
937
+ return new ASTNode('MemberAccess', {
938
+ object: new ASTNode('Identifier', { name: 'db' }),
939
+ property: method
940
+ });
941
+ }
942
+
943
+ parseLogExpression() {
944
+ this.advance(); // log
945
+ let level = 'log';
946
+ if (this.match(T.DOT)) {
947
+ level = this.expect(T.IDENT).value;
948
+ }
949
+ if (this.at(T.LPAREN)) {
950
+ this.advance();
951
+ const args = this.parseArgList();
952
+ this.expect(T.RPAREN);
953
+ return new ASTNode('Call', {
954
+ callee: new ASTNode('MemberAccess', {
955
+ object: new ASTNode('Identifier', { name: 'console' }),
956
+ property: level
957
+ }),
958
+ args
959
+ });
960
+ }
961
+ return new ASTNode('MemberAccess', {
962
+ object: new ASTNode('Identifier', { name: 'console' }),
963
+ property: level
964
+ });
965
+ }
966
+
967
+ parseGroupOrArrow() {
968
+ // Check if this is an arrow function: (params) => body
969
+ const savedPos = this.pos;
970
+ this.advance(); // (
971
+
972
+ // Try to parse as arrow function params
973
+ let isArrow = false;
974
+ let depth = 1;
975
+ let scanPos = this.pos;
976
+ while (scanPos < this.tokens.length && depth > 0) {
977
+ if (this.tokens[scanPos].type === T.LPAREN) depth++;
978
+ if (this.tokens[scanPos].type === T.RPAREN) depth--;
979
+ scanPos++;
980
+ }
981
+ if (scanPos < this.tokens.length && this.tokens[scanPos].type === T.FAT_ARROW) {
982
+ isArrow = true;
983
+ }
984
+
985
+ if (isArrow) {
986
+ const params = [];
987
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
988
+ let type = null;
989
+ if (TYPE_TOKENS.has(this.peek().type)) {
990
+ type = this.advance().value;
991
+ }
992
+ const name = this.expect(T.IDENT).value;
993
+ params.push({ name, type });
994
+ this.match(T.COMMA);
995
+ }
996
+ this.expect(T.RPAREN);
997
+ this.expect(T.FAT_ARROW);
998
+ const body = this.parseExpression();
999
+ return new ASTNode('ArrowFn', { params, body });
1000
+ }
1001
+
1002
+ // Regular grouping
1003
+ this.pos = savedPos;
1004
+ this.advance(); // (
1005
+ const expr = this.parseExpression();
1006
+ this.expect(T.RPAREN);
1007
+ return expr;
1008
+ }
1009
+
1010
+ parseArray() {
1011
+ this.advance(); // [
1012
+ const elements = [];
1013
+ while (!this.at(T.RBRACKET) && !this.at(T.EOF)) {
1014
+ this.skipWhitespace();
1015
+ if (this.at(T.RBRACKET)) break;
1016
+ elements.push(this.parseExpression());
1017
+ this.match(T.COMMA);
1018
+ this.skipWhitespace();
1019
+ }
1020
+ this.expect(T.RBRACKET);
1021
+ return new ASTNode('Array', { elements });
1022
+ }
1023
+
1024
+ parseObject() {
1025
+ this.advance(); // {
1026
+ const properties = [];
1027
+ while (!this.at(T.RBRACE) && !this.at(T.EOF)) {
1028
+ this.skipWhitespace();
1029
+ if (this.at(T.RBRACE)) break;
1030
+
1031
+ if (this.match(T.SPREAD)) {
1032
+ const expr = this.parseExpression();
1033
+ properties.push({ type: 'spread', value: expr });
1034
+ this.match(T.COMMA);
1035
+ this.skipWhitespace();
1036
+ continue;
1037
+ }
1038
+
1039
+ let key;
1040
+ if (this.at(T.STRING)) {
1041
+ key = this.parseString();
1042
+ key = new ASTNode('String', { value: key });
1043
+ } else if (this.at(T.LBRACKET)) {
1044
+ this.advance();
1045
+ key = this.parseExpression();
1046
+ this.expect(T.RBRACKET);
1047
+ key = new ASTNode('Computed', { expr: key });
1048
+ } else {
1049
+ key = this.expect(T.IDENT).value;
1050
+ }
1051
+
1052
+ if (this.match(T.COLON)) {
1053
+ const value = this.parseExpression();
1054
+ properties.push({ type: 'property', key, value });
1055
+ } else {
1056
+ properties.push({ type: 'shorthand', key });
1057
+ }
1058
+ this.match(T.COMMA);
1059
+ this.skipWhitespace();
1060
+ }
1061
+ this.expect(T.RBRACE);
1062
+ return new ASTNode('Object', { properties });
1063
+ }
1064
+
1065
+ parseLambda() {
1066
+ const isAsync = this.at(T.FN_ASYNC);
1067
+ this.advance(); // fn or fn.async
1068
+ this.expect(T.LPAREN);
1069
+ const params = this.parseFnParams();
1070
+ this.expect(T.RPAREN);
1071
+ let returnType = null;
1072
+ if (this.match(T.ARROW)) {
1073
+ returnType = this.parseTypeAnnotation();
1074
+ }
1075
+ this.expect(T.COLON);
1076
+ const body = this.parseBlock();
1077
+ return new ASTNode('Lambda', { params, returnType, body, isAsync });
1078
+ }
1079
+ }