arc-lang 0.5.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/parser.js ADDED
@@ -0,0 +1,779 @@
1
+ // Arc Language Parser - Recursive Descent with Pratt Parsing
2
+ import { TokenType } from "./lexer.js";
3
+ export class ParseError extends Error {
4
+ loc;
5
+ constructor(msg, loc) {
6
+ super(`Parse error at line ${loc.line}, col ${loc.col}: ${msg}`);
7
+ this.loc = loc;
8
+ }
9
+ }
10
+ export class Parser {
11
+ pos = 0;
12
+ tokens;
13
+ constructor(tokens) {
14
+ // Filter out newlines for simpler parsing (treat as whitespace)
15
+ this.tokens = tokens.filter(t => t.type !== TokenType.Newline);
16
+ }
17
+ peek() { return this.tokens[this.pos] ?? this.tokens[this.tokens.length - 1]; }
18
+ at(type) { return this.peek().type === type; }
19
+ loc() { return { line: this.peek().line, col: this.peek().col }; }
20
+ advance() {
21
+ const t = this.peek();
22
+ if (this.pos < this.tokens.length - 1)
23
+ this.pos++;
24
+ return t;
25
+ }
26
+ expect(type, msg) {
27
+ if (!this.at(type)) {
28
+ const t = this.peek();
29
+ throw new ParseError(msg ?? `Expected ${TokenType[type]}, got ${TokenType[t.type]} '${t.value}'`, this.loc());
30
+ }
31
+ return this.advance();
32
+ }
33
+ skipSemicolons() {
34
+ while (this.at(TokenType.Semicolon))
35
+ this.advance();
36
+ }
37
+ parse() {
38
+ const stmts = [];
39
+ this.skipSemicolons();
40
+ while (!this.at(TokenType.EOF)) {
41
+ stmts.push(this.parseStmt());
42
+ this.skipSemicolons();
43
+ }
44
+ return { kind: "Program", stmts };
45
+ }
46
+ parseStmt() {
47
+ this.skipSemicolons();
48
+ const t = this.peek();
49
+ switch (t.type) {
50
+ case TokenType.Let: return this.parseLet();
51
+ case TokenType.Pub: return this.parsePub();
52
+ case TokenType.Fn: return this.parseFn(false);
53
+ case TokenType.Async: return this.parseAsync();
54
+ case TokenType.For: return this.parseFor();
55
+ case TokenType.Do: return this.parseDo();
56
+ case TokenType.Use: return this.parseUse();
57
+ case TokenType.Type: return this.parseType();
58
+ default: {
59
+ const exprLoc = this.loc();
60
+ const expr = this.parseExpr();
61
+ // Check for assignment: expr = value
62
+ if (this.at(TokenType.Assign)) {
63
+ this.advance();
64
+ const value = this.parseExpr();
65
+ if (expr.kind === "Identifier") {
66
+ return { kind: "AssignStmt", target: expr.name, value, loc: exprLoc };
67
+ }
68
+ if (expr.kind === "MemberExpr") {
69
+ return { kind: "MemberAssignStmt", object: expr.object, property: expr.property, value, loc: exprLoc };
70
+ }
71
+ if (expr.kind === "IndexExpr") {
72
+ return { kind: "IndexAssignStmt", object: expr.object, index: expr.index, value, loc: exprLoc };
73
+ }
74
+ throw new ParseError("Invalid assignment target", exprLoc);
75
+ }
76
+ return { kind: "ExprStmt", expr, loc: this.loc() };
77
+ }
78
+ }
79
+ }
80
+ parsePub() {
81
+ const loc = this.loc();
82
+ this.advance(); // consume 'pub'
83
+ if (this.at(TokenType.Fn))
84
+ return this.parseFn(false, undefined, true);
85
+ if (this.at(TokenType.Let))
86
+ return this.parseLet(true);
87
+ if (this.at(TokenType.Type))
88
+ return this.parseType(true);
89
+ if (this.at(TokenType.Async)) {
90
+ this.advance();
91
+ return this.parseFn(true, loc, true);
92
+ }
93
+ throw new ParseError("Expected fn, let, or type after pub", loc);
94
+ }
95
+ parseLet(pub = false) {
96
+ const loc = this.loc();
97
+ this.expect(TokenType.Let);
98
+ let mutable = false;
99
+ if (this.at(TokenType.Mut)) {
100
+ this.advance();
101
+ mutable = true;
102
+ }
103
+ let name;
104
+ if (this.at(TokenType.LBrace)) {
105
+ // Object destructuring
106
+ this.advance();
107
+ const names = [];
108
+ while (!this.at(TokenType.RBrace)) {
109
+ names.push(this.expect(TokenType.Ident).value);
110
+ if (this.at(TokenType.Comma))
111
+ this.advance();
112
+ }
113
+ this.expect(TokenType.RBrace);
114
+ name = { kind: "DestructureTarget", type: "object", names };
115
+ }
116
+ else if (this.at(TokenType.LBracket)) {
117
+ this.advance();
118
+ const names = [];
119
+ while (!this.at(TokenType.RBracket)) {
120
+ names.push(this.expect(TokenType.Ident).value);
121
+ if (this.at(TokenType.Comma))
122
+ this.advance();
123
+ }
124
+ this.expect(TokenType.RBracket);
125
+ name = { kind: "DestructureTarget", type: "array", names };
126
+ }
127
+ else {
128
+ name = this.expect(TokenType.Ident).value;
129
+ }
130
+ this.expect(TokenType.Assign);
131
+ const value = this.parseExpr();
132
+ return { kind: "LetStmt", name, mutable, pub, value, loc };
133
+ }
134
+ parseAsync() {
135
+ const loc = this.loc();
136
+ this.expect(TokenType.Async);
137
+ if (this.at(TokenType.Fn)) {
138
+ return this.parseFn(true, loc);
139
+ }
140
+ // async { body } as expression statement
141
+ const body = this.parseBlock();
142
+ const expr = { kind: "AsyncExpr", body, loc };
143
+ // Check for assignment context — but since we came from parseStmt default would handle it
144
+ // Just wrap as ExprStmt
145
+ return { kind: "ExprStmt", expr, loc };
146
+ }
147
+ parseFn(isAsync = false, asyncLoc, pub = false) {
148
+ const loc = asyncLoc ?? this.loc();
149
+ this.expect(TokenType.Fn);
150
+ const name = this.expect(TokenType.Ident).value;
151
+ this.expect(TokenType.LParen);
152
+ const params = [];
153
+ while (!this.at(TokenType.RParen)) {
154
+ params.push(this.expect(TokenType.Ident).value);
155
+ if (this.at(TokenType.Comma))
156
+ this.advance();
157
+ }
158
+ this.expect(TokenType.RParen);
159
+ let body;
160
+ if (this.at(TokenType.FatArrow)) {
161
+ this.advance();
162
+ body = this.parseExpr();
163
+ }
164
+ else {
165
+ body = this.parseBlock();
166
+ }
167
+ return { kind: "FnStmt", name, params, body, isAsync, pub, loc };
168
+ }
169
+ parseFor() {
170
+ const loc = this.loc();
171
+ this.expect(TokenType.For);
172
+ const variable = this.expect(TokenType.Ident).value;
173
+ this.expect(TokenType.In);
174
+ const iterable = this.parseExpr();
175
+ const body = this.parseBlock();
176
+ return { kind: "ForStmt", variable, iterable, body, loc };
177
+ }
178
+ parseDo() {
179
+ const loc = this.loc();
180
+ this.expect(TokenType.Do);
181
+ const body = this.parseBlock();
182
+ const isWhile = this.at(TokenType.While);
183
+ if (!isWhile)
184
+ this.expect(TokenType.Until);
185
+ else
186
+ this.advance();
187
+ const condition = this.parseExpr();
188
+ return { kind: "DoStmt", body, condition, isWhile, loc };
189
+ }
190
+ parseUse() {
191
+ const loc = this.loc();
192
+ this.expect(TokenType.Use);
193
+ const path = [this.expect(TokenType.Ident).value];
194
+ while (this.at(TokenType.Slash)) {
195
+ this.advance();
196
+ path.push(this.expect(TokenType.Ident).value);
197
+ }
198
+ // Also support legacy dot separator
199
+ while (this.at(TokenType.Dot)) {
200
+ this.advance();
201
+ path.push(this.expect(TokenType.Ident).value);
202
+ }
203
+ let imports;
204
+ let wildcard;
205
+ if (this.at(TokenType.Colon)) {
206
+ this.advance();
207
+ if (this.at(TokenType.Star)) {
208
+ this.advance();
209
+ wildcard = true;
210
+ }
211
+ else {
212
+ imports = [this.expect(TokenType.Ident).value];
213
+ while (this.at(TokenType.Comma)) {
214
+ this.advance();
215
+ imports.push(this.expect(TokenType.Ident).value);
216
+ }
217
+ }
218
+ }
219
+ return { kind: "UseStmt", path, imports, wildcard, loc };
220
+ }
221
+ parseType(pub = false) {
222
+ const loc = this.loc();
223
+ this.expect(TokenType.Type);
224
+ const name = this.expect(TokenType.Ident).value;
225
+ this.expect(TokenType.Assign);
226
+ const def = this.parseTypeExpr();
227
+ return { kind: "TypeStmt", name, pub, def, loc };
228
+ }
229
+ parseTypeExpr() {
230
+ let typeExpr = this.parseTypeAtom();
231
+ // Check for enum/union: Type | Type
232
+ if (this.at(TokenType.Bar)) {
233
+ const variants = [typeExpr];
234
+ while (this.at(TokenType.Bar)) {
235
+ this.advance();
236
+ variants.push(this.parseTypeAtom());
237
+ }
238
+ // Check if all are NamedType — treat as enum if they look like variants
239
+ const allNamed = variants.every(v => v.kind === "NamedType");
240
+ if (allNamed) {
241
+ return { kind: "UnionType", variants };
242
+ }
243
+ return { kind: "UnionType", variants };
244
+ }
245
+ // Check for constraints: where / matching
246
+ if (this.at(TokenType.Where)) {
247
+ this.advance();
248
+ const predicate = this.parseExpr();
249
+ return { kind: "ConstrainedType", base: typeExpr, constraint: "where", predicate };
250
+ }
251
+ if (this.at(TokenType.Matching)) {
252
+ this.advance();
253
+ // Expect regex token or string
254
+ let predicate;
255
+ if (this.at(TokenType.Regex)) {
256
+ const regexVal = this.advance().value;
257
+ predicate = { kind: "StringLiteral", value: regexVal, loc: this.loc() };
258
+ }
259
+ else if (this.at(TokenType.String)) {
260
+ predicate = { kind: "StringLiteral", value: this.advance().value, loc: this.loc() };
261
+ }
262
+ else {
263
+ throw new ParseError("Expected regex or string after 'matching'", this.loc());
264
+ }
265
+ return { kind: "ConstrainedType", base: typeExpr, constraint: "matching", predicate };
266
+ }
267
+ return typeExpr;
268
+ }
269
+ parseTypeAtom() {
270
+ // Record type: { name: Type, ... }
271
+ if (this.at(TokenType.LBrace)) {
272
+ this.advance();
273
+ const fields = [];
274
+ while (!this.at(TokenType.RBrace) && !this.at(TokenType.EOF)) {
275
+ const fieldName = this.expect(TokenType.Ident).value;
276
+ this.expect(TokenType.Colon);
277
+ const fieldType = this.parseTypeExpr();
278
+ fields.push({ name: fieldName, type: fieldType });
279
+ if (this.at(TokenType.Comma))
280
+ this.advance();
281
+ }
282
+ this.expect(TokenType.RBrace);
283
+ return { kind: "RecordType", fields };
284
+ }
285
+ // Function type: (Type, ...) -> Type
286
+ if (this.at(TokenType.LParen)) {
287
+ const saved = this.pos;
288
+ try {
289
+ this.advance();
290
+ const params = [];
291
+ while (!this.at(TokenType.RParen) && !this.at(TokenType.EOF)) {
292
+ params.push(this.parseTypeExpr());
293
+ if (this.at(TokenType.Comma))
294
+ this.advance();
295
+ }
296
+ this.expect(TokenType.RParen);
297
+ if (this.at(TokenType.Arrow)) {
298
+ this.advance();
299
+ const ret = this.parseTypeExpr();
300
+ return { kind: "FunctionType", params, ret };
301
+ }
302
+ // Not a function type, restore
303
+ this.pos = saved;
304
+ }
305
+ catch {
306
+ this.pos = saved;
307
+ }
308
+ }
309
+ // Named type (possibly with enum variant params or generics)
310
+ if (this.at(TokenType.Ident)) {
311
+ const name = this.advance().value;
312
+ // Generic: Name<Type, ...>
313
+ if (this.at(TokenType.Lt)) {
314
+ this.advance();
315
+ const params = [];
316
+ while (!this.at(TokenType.Gt) && !this.at(TokenType.EOF)) {
317
+ params.push(this.parseTypeExpr());
318
+ if (this.at(TokenType.Comma))
319
+ this.advance();
320
+ }
321
+ this.expect(TokenType.Gt);
322
+ return { kind: "GenericType", name, params };
323
+ }
324
+ // Enum variant with params: Name(Type, ...)
325
+ if (this.at(TokenType.LParen)) {
326
+ // This is part of enum parsing — but at atom level just return named
327
+ // Enum variants are handled at the union level
328
+ // Actually let's peek if this is in a union context
329
+ // For now, parse variant params
330
+ this.advance();
331
+ const params = [];
332
+ while (!this.at(TokenType.RParen) && !this.at(TokenType.EOF)) {
333
+ params.push(this.parseTypeExpr());
334
+ if (this.at(TokenType.Comma))
335
+ this.advance();
336
+ }
337
+ this.expect(TokenType.RParen);
338
+ return { kind: "EnumType", variants: [{ name, params }] };
339
+ }
340
+ return { kind: "NamedType", name };
341
+ }
342
+ throw new ParseError(`Expected type expression, got ${TokenType[this.peek().type]}`, this.loc());
343
+ }
344
+ parseBlock() {
345
+ const loc = this.loc();
346
+ this.expect(TokenType.LBrace);
347
+ const stmts = [];
348
+ this.skipSemicolons();
349
+ while (!this.at(TokenType.RBrace) && !this.at(TokenType.EOF)) {
350
+ stmts.push(this.parseStmt());
351
+ this.skipSemicolons();
352
+ }
353
+ this.expect(TokenType.RBrace);
354
+ return { kind: "BlockExpr", stmts, loc };
355
+ }
356
+ // ---- Pratt Expression Parsing ----
357
+ parseExpr(minPrec = 0) {
358
+ let left = this.parsePrefix();
359
+ while (true) {
360
+ const prec = this.infixPrec();
361
+ if (prec < minPrec)
362
+ break;
363
+ const t = this.peek();
364
+ if (t.type === TokenType.Pipe) {
365
+ this.advance();
366
+ const right = this.parseExpr(prec + 1);
367
+ left = { kind: "PipelineExpr", left, right, loc: left.loc };
368
+ continue;
369
+ }
370
+ if (t.type === TokenType.Range) {
371
+ this.advance();
372
+ const right = this.parseExpr(prec + 1);
373
+ left = { kind: "RangeExpr", start: left, end: right, loc: left.loc };
374
+ continue;
375
+ }
376
+ // Postfix: function call
377
+ if (t.type === TokenType.LParen) {
378
+ this.advance();
379
+ const args = [];
380
+ while (!this.at(TokenType.RParen)) {
381
+ args.push(this.parseExpr());
382
+ if (this.at(TokenType.Comma))
383
+ this.advance();
384
+ }
385
+ this.expect(TokenType.RParen);
386
+ left = { kind: "CallExpr", callee: left, args, loc: left.loc };
387
+ continue;
388
+ }
389
+ // Postfix: index
390
+ if (t.type === TokenType.LBracket) {
391
+ this.advance();
392
+ const index = this.parseExpr();
393
+ this.expect(TokenType.RBracket);
394
+ left = { kind: "IndexExpr", object: left, index, loc: left.loc };
395
+ continue;
396
+ }
397
+ // Postfix: member access
398
+ if (t.type === TokenType.Dot) {
399
+ this.advance();
400
+ const prop = this.expect(TokenType.Ident).value;
401
+ left = { kind: "MemberExpr", object: left, property: prop, loc: left.loc };
402
+ continue;
403
+ }
404
+ // Binary operators
405
+ const op = this.binaryOp();
406
+ if (op) {
407
+ this.advance();
408
+ const right = this.parseExpr(prec + 1);
409
+ left = { kind: "BinaryExpr", op, left, right, loc: left.loc };
410
+ continue;
411
+ }
412
+ break;
413
+ }
414
+ return left;
415
+ }
416
+ infixPrec() {
417
+ const t = this.peek().type;
418
+ switch (t) {
419
+ case TokenType.Or: return 1;
420
+ case TokenType.And: return 2;
421
+ case TokenType.Eq:
422
+ case TokenType.Neq:
423
+ case TokenType.Lt:
424
+ case TokenType.Gt:
425
+ case TokenType.Lte:
426
+ case TokenType.Gte: return 3;
427
+ case TokenType.Concat: return 4;
428
+ case TokenType.Plus:
429
+ case TokenType.Minus: return 5;
430
+ case TokenType.Star:
431
+ case TokenType.Slash:
432
+ case TokenType.Percent: return 6;
433
+ case TokenType.Power: return 7;
434
+ case TokenType.Range: return 8;
435
+ case TokenType.Dot:
436
+ case TokenType.LBracket:
437
+ case TokenType.LParen: return 10;
438
+ case TokenType.Pipe: return 0; // lowest
439
+ default: return -1;
440
+ }
441
+ }
442
+ binaryOp() {
443
+ const t = this.peek().type;
444
+ const map = {
445
+ [TokenType.Plus]: "+", [TokenType.Minus]: "-", [TokenType.Star]: "*",
446
+ [TokenType.Slash]: "/", [TokenType.Percent]: "%", [TokenType.Power]: "**",
447
+ [TokenType.Eq]: "==", [TokenType.Neq]: "!=", [TokenType.Lt]: "<",
448
+ [TokenType.Gt]: ">", [TokenType.Lte]: "<=", [TokenType.Gte]: ">=",
449
+ [TokenType.And]: "and", [TokenType.Or]: "or", [TokenType.Concat]: "++",
450
+ };
451
+ return map[t] ?? null;
452
+ }
453
+ parsePrefix() {
454
+ const t = this.peek();
455
+ const loc = this.loc();
456
+ // Unary operators
457
+ if (t.type === TokenType.Minus) {
458
+ this.advance();
459
+ return { kind: "UnaryExpr", op: "-", operand: this.parseExpr(8), loc };
460
+ }
461
+ if (t.type === TokenType.Not) {
462
+ this.advance();
463
+ return { kind: "UnaryExpr", op: "not", operand: this.parseExpr(8), loc };
464
+ }
465
+ // Literals
466
+ if (t.type === TokenType.Int) {
467
+ this.advance();
468
+ return { kind: "IntLiteral", value: parseInt(t.value), loc };
469
+ }
470
+ if (t.type === TokenType.Float) {
471
+ this.advance();
472
+ return { kind: "FloatLiteral", value: parseFloat(t.value), loc };
473
+ }
474
+ if (t.type === TokenType.True) {
475
+ this.advance();
476
+ return { kind: "BoolLiteral", value: true, loc };
477
+ }
478
+ if (t.type === TokenType.False) {
479
+ this.advance();
480
+ return { kind: "BoolLiteral", value: false, loc };
481
+ }
482
+ if (t.type === TokenType.NilKw) {
483
+ this.advance();
484
+ return { kind: "NilLiteral", loc };
485
+ }
486
+ if (t.type === TokenType.String) {
487
+ this.advance();
488
+ return { kind: "StringLiteral", value: t.value, loc };
489
+ }
490
+ // String interpolation
491
+ if (t.type === TokenType.StringInterpStart) {
492
+ return this.parseStringInterp();
493
+ }
494
+ // Identifier (possibly lambda)
495
+ if (t.type === TokenType.Ident) {
496
+ // Check for lambda: ident => expr
497
+ if (this.tokens[this.pos + 1]?.type === TokenType.FatArrow) {
498
+ const param = this.advance().value;
499
+ this.advance(); // =>
500
+ const body = this.parseExpr();
501
+ return { kind: "LambdaExpr", params: [param], body, loc };
502
+ }
503
+ this.advance();
504
+ return { kind: "Identifier", name: t.value, loc };
505
+ }
506
+ // Parenthesized expression or multi-param lambda
507
+ if (t.type === TokenType.LParen) {
508
+ this.advance();
509
+ // Check for lambda: (a, b) => expr
510
+ if (this.at(TokenType.RParen)) {
511
+ this.advance();
512
+ if (this.at(TokenType.FatArrow)) {
513
+ this.advance();
514
+ const body = this.parseExpr();
515
+ return { kind: "LambdaExpr", params: [], body, loc };
516
+ }
517
+ // Empty parens as nil?
518
+ return { kind: "NilLiteral", loc };
519
+ }
520
+ // Try to detect lambda
521
+ const saved = this.pos;
522
+ let isLambda = false;
523
+ try {
524
+ const names = [];
525
+ if (this.at(TokenType.Ident)) {
526
+ names.push(this.advance().value);
527
+ while (this.at(TokenType.Comma)) {
528
+ this.advance();
529
+ names.push(this.expect(TokenType.Ident).value);
530
+ }
531
+ if (this.at(TokenType.RParen)) {
532
+ this.advance();
533
+ if (this.at(TokenType.FatArrow)) {
534
+ this.advance();
535
+ const body = this.parseExpr();
536
+ return { kind: "LambdaExpr", params: names, body, loc };
537
+ }
538
+ }
539
+ }
540
+ }
541
+ catch { }
542
+ this.pos = saved;
543
+ const expr = this.parseExpr();
544
+ this.expect(TokenType.RParen);
545
+ return expr;
546
+ }
547
+ // List literal or comprehension
548
+ if (t.type === TokenType.LBracket) {
549
+ return this.parseListOrComprehension();
550
+ }
551
+ // Map literal or block
552
+ if (t.type === TokenType.LBrace) {
553
+ return this.parseMapOrBlock();
554
+ }
555
+ // If expression
556
+ if (t.type === TokenType.If) {
557
+ return this.parseIf();
558
+ }
559
+ // Match expression
560
+ if (t.type === TokenType.Match) {
561
+ return this.parseMatch();
562
+ }
563
+ // Async expression: async { body }
564
+ if (t.type === TokenType.Async) {
565
+ this.advance();
566
+ const body = this.parseBlock();
567
+ return { kind: "AsyncExpr", body, loc };
568
+ }
569
+ // Await expression: await expr
570
+ if (t.type === TokenType.Await) {
571
+ this.advance();
572
+ const expr = this.parseExpr(8); // high precedence
573
+ return { kind: "AwaitExpr", expr, loc };
574
+ }
575
+ // Fetch expression: fetch [expr1, expr2, ...]
576
+ if (t.type === TokenType.Fetch) {
577
+ this.advance();
578
+ this.expect(TokenType.LBracket);
579
+ const targets = [];
580
+ while (!this.at(TokenType.RBracket)) {
581
+ targets.push(this.parseExpr());
582
+ if (this.at(TokenType.Comma))
583
+ this.advance();
584
+ }
585
+ this.expect(TokenType.RBracket);
586
+ return { kind: "FetchExpr", targets, loc };
587
+ }
588
+ // Tool call: @GET "url" or @ident(args)
589
+ if (t.type === TokenType.At) {
590
+ return this.parseToolCall();
591
+ }
592
+ throw new ParseError(`Unexpected token: ${TokenType[t.type]} '${t.value}'`, loc);
593
+ }
594
+ parseStringInterp() {
595
+ const loc = this.loc();
596
+ this.advance(); // StringInterpStart
597
+ const parts = [];
598
+ while (!this.at(TokenType.StringInterpEnd) && !this.at(TokenType.EOF)) {
599
+ if (this.at(TokenType.StringInterpPart)) {
600
+ parts.push(this.advance().value);
601
+ }
602
+ else if (this.at(TokenType.Ident)) {
603
+ // This is an interpolated expression identifier
604
+ const identToken = this.advance();
605
+ // Try to parse a more complex expression from the token value
606
+ parts.push({ kind: "Identifier", name: identToken.value, loc: { line: identToken.line, col: identToken.col } });
607
+ }
608
+ else {
609
+ this.advance(); // skip unexpected
610
+ }
611
+ }
612
+ if (this.at(TokenType.StringInterpEnd))
613
+ this.advance();
614
+ return { kind: "StringInterp", parts, loc };
615
+ }
616
+ parseListOrComprehension() {
617
+ const loc = this.loc();
618
+ this.expect(TokenType.LBracket);
619
+ if (this.at(TokenType.RBracket)) {
620
+ this.advance();
621
+ return { kind: "ListLiteral", elements: [], loc };
622
+ }
623
+ const first = this.parseExpr();
624
+ // Check for list comprehension: [expr for x in iter if cond]
625
+ if (this.at(TokenType.For)) {
626
+ this.advance();
627
+ const variable = this.expect(TokenType.Ident).value;
628
+ this.expect(TokenType.In);
629
+ const iterable = this.parseExpr();
630
+ let filter;
631
+ if (this.at(TokenType.If)) {
632
+ this.advance();
633
+ filter = this.parseExpr();
634
+ }
635
+ this.expect(TokenType.RBracket);
636
+ return { kind: "ListComprehension", expr: first, variable, iterable, filter, loc };
637
+ }
638
+ // Regular list
639
+ const elements = [first];
640
+ while (this.at(TokenType.Comma)) {
641
+ this.advance();
642
+ if (this.at(TokenType.RBracket))
643
+ break;
644
+ elements.push(this.parseExpr());
645
+ }
646
+ this.expect(TokenType.RBracket);
647
+ return { kind: "ListLiteral", elements, loc };
648
+ }
649
+ parseMapOrBlock() {
650
+ const loc = this.loc();
651
+ // Peek ahead to determine if map literal (key: value) or block
652
+ const saved = this.pos;
653
+ this.advance(); // skip {
654
+ // Empty braces = empty map
655
+ if (this.at(TokenType.RBrace)) {
656
+ this.advance();
657
+ return { kind: "MapLiteral", entries: [], loc };
658
+ }
659
+ // Check if it's a map: identifier followed by colon
660
+ if (this.at(TokenType.Ident) && this.tokens[this.pos + 1]?.type === TokenType.Colon) {
661
+ // It's a map literal
662
+ const entries = [];
663
+ while (!this.at(TokenType.RBrace) && !this.at(TokenType.EOF)) {
664
+ const key = this.expect(TokenType.Ident).value;
665
+ this.expect(TokenType.Colon);
666
+ const value = this.parseExpr();
667
+ entries.push({ key, value });
668
+ if (this.at(TokenType.Comma))
669
+ this.advance();
670
+ }
671
+ this.expect(TokenType.RBrace);
672
+ return { kind: "MapLiteral", entries, loc };
673
+ }
674
+ // It's a block
675
+ this.pos = saved;
676
+ return this.parseBlock();
677
+ }
678
+ parseIf() {
679
+ const loc = this.loc();
680
+ this.expect(TokenType.If);
681
+ const condition = this.parseExpr(0);
682
+ const then = this.parseBlock();
683
+ let else_;
684
+ if (this.at(TokenType.El)) {
685
+ this.advance();
686
+ if (this.at(TokenType.If)) {
687
+ else_ = this.parseIf();
688
+ }
689
+ else {
690
+ else_ = this.parseBlock();
691
+ }
692
+ }
693
+ return { kind: "IfExpr", condition, then, else_, loc };
694
+ }
695
+ parseMatch() {
696
+ const loc = this.loc();
697
+ this.expect(TokenType.Match);
698
+ const subject = this.parseExpr();
699
+ this.expect(TokenType.LBrace);
700
+ const arms = [];
701
+ while (!this.at(TokenType.RBrace) && !this.at(TokenType.EOF)) {
702
+ const pattern = this.parsePattern();
703
+ let guard;
704
+ if (this.at(TokenType.If)) {
705
+ this.advance();
706
+ guard = this.parseExpr();
707
+ }
708
+ this.expect(TokenType.FatArrow);
709
+ const body = this.parseExpr();
710
+ arms.push({ pattern, guard, body });
711
+ if (this.at(TokenType.Comma))
712
+ this.advance();
713
+ }
714
+ this.expect(TokenType.RBrace);
715
+ return { kind: "MatchExpr", subject, arms, loc };
716
+ }
717
+ parsePattern() {
718
+ const loc = this.loc();
719
+ const t = this.peek();
720
+ if (t.type === TokenType.Ident && t.value === "_") {
721
+ this.advance();
722
+ return { kind: "WildcardPattern", loc };
723
+ }
724
+ if (t.type === TokenType.Int || t.type === TokenType.Float) {
725
+ this.advance();
726
+ return { kind: "LiteralPattern", value: parseFloat(t.value), loc };
727
+ }
728
+ if (t.type === TokenType.String) {
729
+ this.advance();
730
+ return { kind: "LiteralPattern", value: t.value, loc };
731
+ }
732
+ if (t.type === TokenType.True) {
733
+ this.advance();
734
+ return { kind: "LiteralPattern", value: true, loc };
735
+ }
736
+ if (t.type === TokenType.False) {
737
+ this.advance();
738
+ return { kind: "LiteralPattern", value: false, loc };
739
+ }
740
+ if (t.type === TokenType.NilKw) {
741
+ this.advance();
742
+ return { kind: "LiteralPattern", value: null, loc };
743
+ }
744
+ if (t.type === TokenType.Ident) {
745
+ this.advance();
746
+ return { kind: "BindingPattern", name: t.value, loc };
747
+ }
748
+ throw new ParseError(`Expected pattern, got ${TokenType[t.type]}`, loc);
749
+ }
750
+ parseToolCall() {
751
+ const loc = this.loc();
752
+ this.expect(TokenType.At);
753
+ const method = this.expect(TokenType.Ident).value;
754
+ // @ident(args) - custom tool call
755
+ if (this.at(TokenType.LParen)) {
756
+ this.advance();
757
+ const args = [];
758
+ while (!this.at(TokenType.RParen)) {
759
+ args.push(this.parseExpr());
760
+ if (this.at(TokenType.Comma))
761
+ this.advance();
762
+ }
763
+ this.expect(TokenType.RParen);
764
+ const arg = args.length === 1 ? args[0] :
765
+ { kind: "ListLiteral", elements: args, loc };
766
+ return { kind: "ToolCallExpr", method, arg, loc };
767
+ }
768
+ // @METHOD "url" {body?}
769
+ const arg = this.parseExpr(9); // high precedence to grab just the string
770
+ let body;
771
+ if (this.at(TokenType.LBrace)) {
772
+ body = this.parseMapOrBlock();
773
+ }
774
+ return { kind: "ToolCallExpr", method, arg, body, loc };
775
+ }
776
+ }
777
+ export function parse(tokens) {
778
+ return new Parser(tokens).parse();
779
+ }