techscript 1.0.3__py3-none-any.whl

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.
techscript/parser.py ADDED
@@ -0,0 +1,637 @@
1
+ """TechScript Parser — recursive-descent parser with precedence climbing.
2
+
3
+ Consumes a list of ``Token`` objects from the lexer and produces an AST
4
+ (``Program`` node) as defined in ``ast_nodes``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from techscript.tokens import Token, TokenType
10
+ from techscript.ast_nodes import *
11
+ from techscript.errors import ParseError
12
+
13
+
14
+ class Parser:
15
+ """Recursive-descent parser for TechScript."""
16
+
17
+ def __init__(self, tokens: list[Token]) -> None:
18
+ self.tokens = tokens
19
+ self.pos = 0
20
+
21
+ # ------------------------------------------------------------------
22
+ # Helpers
23
+ # ------------------------------------------------------------------
24
+
25
+ def _peek(self) -> Token:
26
+ return self.tokens[self.pos]
27
+
28
+ def _advance(self) -> Token:
29
+ tok = self.tokens[self.pos]
30
+ self.pos += 1
31
+ return tok
32
+
33
+ def _expect(self, tt: TokenType, value: str | None = None) -> Token:
34
+ tok = self._peek()
35
+ if tok.type != tt:
36
+ raise ParseError(
37
+ f"Expected {tt.name}, got {tok.type.name} ('{tok.value}')",
38
+ line=tok.line, column=tok.column,
39
+ )
40
+ if value is not None and tok.value != value:
41
+ raise ParseError(
42
+ f"Expected '{value}', got '{tok.value}'",
43
+ line=tok.line, column=tok.column,
44
+ )
45
+ return self._advance()
46
+
47
+ def _match(self, tt: TokenType, value: str | None = None) -> Token | None:
48
+ tok = self._peek()
49
+ if tok.type == tt and (value is None or tok.value == value):
50
+ return self._advance()
51
+ return None
52
+
53
+ def _kw(self, value: str) -> Token | None:
54
+ """Match a keyword token with a specific value."""
55
+ return self._match(TokenType.KEYWORD, value)
56
+
57
+ def _skip_nl(self) -> None:
58
+ while self._peek().type == TokenType.NEWLINE:
59
+ self._advance()
60
+
61
+ def _at_end(self) -> bool:
62
+ return self._peek().type == TokenType.EOF
63
+
64
+ # ------------------------------------------------------------------
65
+ # Public entry point
66
+ # ------------------------------------------------------------------
67
+
68
+ def parse(self) -> Program:
69
+ self._skip_nl()
70
+ body: list[Any] = []
71
+ while not self._at_end():
72
+ stmt = self._parse_statement()
73
+ if stmt is not None:
74
+ body.append(stmt)
75
+ self._skip_nl()
76
+ return Program(body=body)
77
+
78
+ # ------------------------------------------------------------------
79
+ # Statement parsing
80
+ # ------------------------------------------------------------------
81
+
82
+ def _parse_statement(self) -> Any:
83
+ tok = self._peek()
84
+
85
+ if tok.type == TokenType.KEYWORD:
86
+ handler = {
87
+ "say": self._parse_say,
88
+ "make": self._parse_set,
89
+ "keep": self._parse_const,
90
+ "when": self._parse_if,
91
+ "unless": self._parse_unless,
92
+ "each": self._parse_for,
93
+ "repeat": self._parse_while,
94
+ "until": self._parse_until,
95
+ "build": self._parse_fn,
96
+ "model": self._parse_class,
97
+ "send": self._parse_return,
98
+ "stop": lambda: (self._advance(), BreakStmt())[1],
99
+ "skip": lambda: (self._advance(), SkipStmt())[1],
100
+ "pass": lambda: (self._advance(), PassStmt())[1],
101
+ "attempt": self._parse_try,
102
+ "fail": self._parse_throw,
103
+ "match": self._parse_match,
104
+ "use": self._parse_import,
105
+ "take": self._parse_from_import,
106
+ "drop": self._parse_del,
107
+ "defer": self._parse_defer,
108
+ }.get(tok.value)
109
+ if handler:
110
+ return handler()
111
+
112
+ return self._parse_expression_statement()
113
+
114
+ # --- simple statements ---
115
+
116
+ def _parse_say(self) -> SayStmt:
117
+ self._advance() # 'say'
118
+ values = [self._parse_expression()]
119
+ while self._match(TokenType.COMMA):
120
+ values.append(self._parse_expression())
121
+ return SayStmt(values=values)
122
+
123
+ def _parse_set(self) -> SetStmt:
124
+ self._advance() # 'make'
125
+ name = self._expect(TokenType.IDENTIFIER).value
126
+ self._expect(TokenType.ASSIGN)
127
+ value = self._parse_expression()
128
+ return SetStmt(name=name, value=value)
129
+
130
+ def _parse_const(self) -> ConstStmt:
131
+ self._advance() # 'keep'
132
+ name = self._expect(TokenType.IDENTIFIER).value
133
+ self._expect(TokenType.ASSIGN)
134
+ value = self._parse_expression()
135
+ return ConstStmt(name=name, value=value)
136
+
137
+ def _parse_return(self) -> ReturnStmt:
138
+ self._advance() # 'send'
139
+ value = None
140
+ if self._peek().type not in (TokenType.NEWLINE, TokenType.EOF, TokenType.RBRACE):
141
+ value = self._parse_expression()
142
+ return ReturnStmt(value=value)
143
+
144
+ def _parse_throw(self) -> ThrowStmt:
145
+ self._advance() # 'fail'
146
+ return ThrowStmt(value=self._parse_expression())
147
+
148
+ def _parse_del(self) -> DelStmt:
149
+ self._advance() # 'drop'
150
+ return DelStmt(name=self._expect(TokenType.IDENTIFIER).value)
151
+
152
+ def _parse_defer(self) -> DeferStmt:
153
+ self._advance() # 'defer'
154
+ return DeferStmt(expression=self._parse_expression())
155
+
156
+ def _parse_import(self) -> ImportStmt:
157
+ self._advance() # 'import'
158
+ module = self._expect(TokenType.IDENTIFIER).value
159
+ while self._match(TokenType.DOT):
160
+ module += "." + self._expect(TokenType.IDENTIFIER).value
161
+ alias = None
162
+ if self._kw("as"):
163
+ alias = self._expect(TokenType.IDENTIFIER).value
164
+ return ImportStmt(module=module, alias=alias)
165
+
166
+ def _parse_from_import(self) -> FromImportStmt:
167
+ self._advance() # 'from'
168
+ module = self._expect(TokenType.IDENTIFIER).value
169
+ while self._match(TokenType.DOT):
170
+ module += "." + self._expect(TokenType.IDENTIFIER).value
171
+ self._expect(TokenType.KEYWORD, "import")
172
+ names = [self._expect(TokenType.IDENTIFIER).value]
173
+ while self._match(TokenType.COMMA):
174
+ names.append(self._expect(TokenType.IDENTIFIER).value)
175
+ return FromImportStmt(module=module, names=names)
176
+
177
+ def _parse_export(self) -> ExportStmt:
178
+ self._advance() # 'export'
179
+ return ExportStmt(declaration=self._parse_statement())
180
+
181
+ # --- compound statements ---
182
+
183
+ def _parse_if(self) -> IfStmt:
184
+ self._advance() # 'when'
185
+ condition = self._parse_expression()
186
+ body = self._parse_block()
187
+ elif_clauses: list[tuple[Any, list]] = []
188
+ while self._kw("alt"):
189
+ ec = self._parse_expression()
190
+ eb = self._parse_block()
191
+ elif_clauses.append((ec, eb))
192
+ else_body = None
193
+ if self._kw("else"):
194
+ else_body = self._parse_block()
195
+ return IfStmt(condition, body, elif_clauses, else_body)
196
+
197
+ def _parse_unless(self) -> IfStmt:
198
+ self._advance() # 'unless'
199
+ condition = self._parse_expression()
200
+ body = self._parse_block()
201
+ # unless X ≡ if not X
202
+ return IfStmt(condition=UnaryOp("not", condition), body=body)
203
+
204
+ def _parse_for(self) -> ForStmt:
205
+ self._advance() # 'each'
206
+ var_name = self._expect(TokenType.IDENTIFIER).value
207
+ self._expect(TokenType.KEYWORD, "in")
208
+ iterable = self._parse_expression()
209
+ body = self._parse_block()
210
+ return ForStmt(var_name=var_name, iterable=iterable, body=body)
211
+
212
+ def _parse_while(self) -> WhileStmt:
213
+ self._advance() # 'repeat'
214
+ condition = self._parse_expression()
215
+ body = self._parse_block()
216
+ return WhileStmt(condition=condition, body=body)
217
+
218
+ def _parse_until(self) -> WhileStmt:
219
+ self._advance() # 'until'
220
+ condition = self._parse_expression()
221
+ body = self._parse_block()
222
+ # until X ≡ while not X
223
+ return WhileStmt(condition=UnaryOp("not", condition), body=body)
224
+
225
+ def _parse_fn(self) -> FnStmt:
226
+ self._advance() # 'build'
227
+ name = self._expect(TokenType.IDENTIFIER).value
228
+ self._expect(TokenType.LPAREN)
229
+ params = self._parse_param_list()
230
+ self._expect(TokenType.RPAREN)
231
+ body = self._parse_block()
232
+ return FnStmt(name=name, params=params, body=body)
233
+
234
+ def _parse_class(self) -> ClassStmt:
235
+ self._advance() # 'model'
236
+ name = self._expect(TokenType.IDENTIFIER).value
237
+ parent = None
238
+ if self._match(TokenType.LPAREN):
239
+ parent = self._expect(TokenType.IDENTIFIER).value
240
+ self._expect(TokenType.RPAREN)
241
+ body = self._parse_block()
242
+ return ClassStmt(name=name, parent=parent, body=body)
243
+
244
+ def _parse_try(self) -> TryStmt:
245
+ self._advance() # 'attempt'
246
+ body = self._parse_block()
247
+ catch_var = None
248
+ catch_body: list[Any] = []
249
+ finally_body: list[Any] | None = None
250
+ if self._kw("rescue"):
251
+ if self._peek().type == TokenType.IDENTIFIER:
252
+ catch_var = self._advance().value
253
+ catch_body = self._parse_block()
254
+ if self._kw("always"):
255
+ finally_body = self._parse_block()
256
+ return TryStmt(body, catch_var, catch_body, finally_body)
257
+
258
+ def _parse_match(self) -> MatchStmt:
259
+ self._advance() # 'match'
260
+ subject = self._parse_expression()
261
+ self._expect(TokenType.LBRACE)
262
+ self._skip_nl()
263
+ cases: list[tuple[Any, list]] = []
264
+ while self._kw("case"):
265
+ pattern = self._parse_expression()
266
+ case_body = self._parse_block()
267
+ cases.append((pattern, case_body))
268
+ self._skip_nl()
269
+ self._expect(TokenType.RBRACE)
270
+ return MatchStmt(subject=subject, cases=cases)
271
+
272
+ def _parse_guard(self) -> GuardStmt:
273
+ self._advance() # 'guard'
274
+ condition = self._parse_expression()
275
+ self._expect(TokenType.KEYWORD, "else")
276
+ body = self._parse_block()
277
+ return GuardStmt(condition=condition, else_body=body)
278
+
279
+ def _parse_with(self) -> WithStmt:
280
+ self._advance() # 'with'
281
+ expr = self._parse_expression()
282
+ self._expect(TokenType.KEYWORD, "as")
283
+ var = self._expect(TokenType.IDENTIFIER).value
284
+ body = self._parse_block()
285
+ return WithStmt(expression=expr, var_name=var, body=body)
286
+
287
+ # --- blocks ---
288
+
289
+ def _parse_block(self) -> list[Any]:
290
+ self._skip_nl()
291
+ self._expect(TokenType.LBRACE)
292
+ stmts: list[Any] = []
293
+ while self._peek().type not in (TokenType.RBRACE, TokenType.EOF):
294
+ self._skip_nl()
295
+ if self._peek().type in (TokenType.RBRACE, TokenType.EOF):
296
+ break
297
+ stmt = self._parse_statement()
298
+ if stmt is not None:
299
+ stmts.append(stmt)
300
+ self._skip_nl()
301
+ if self._peek().type == TokenType.RBRACE:
302
+ self._advance()
303
+ return stmts
304
+
305
+ def _parse_param_list(self) -> list[Param]:
306
+ params: list[Param] = []
307
+ if self._peek().type == TokenType.RPAREN:
308
+ return params
309
+ # skip 'self' as a pseudo-param (kept for class methods)
310
+ if self._peek().type == TokenType.KEYWORD and self._peek().value == "self":
311
+ self._advance()
312
+ params.append(Param(name="self"))
313
+ if not self._match(TokenType.COMMA):
314
+ return params
315
+ params.append(self._parse_one_param())
316
+ while self._match(TokenType.COMMA):
317
+ params.append(self._parse_one_param())
318
+ return params
319
+
320
+ def _parse_one_param(self) -> Param:
321
+ name = self._expect(TokenType.IDENTIFIER).value
322
+ default = None
323
+ if self._match(TokenType.ASSIGN):
324
+ default = self._parse_expression()
325
+ return Param(name=name, default=default)
326
+
327
+ # --- expression statement / assignment ---
328
+
329
+ def _parse_expression_statement(self) -> Any:
330
+ expr = self._parse_expression()
331
+ assign_ops = {
332
+ TokenType.ASSIGN, TokenType.PLUS_ASSIGN,
333
+ TokenType.MINUS_ASSIGN, TokenType.STAR_ASSIGN,
334
+ TokenType.SLASH_ASSIGN,
335
+ }
336
+ if self._peek().type in assign_ops:
337
+ op_tok = self._advance()
338
+ value = self._parse_expression()
339
+ return AssignStmt(target=expr, op=op_tok.value, value=value)
340
+ return ExpressionStmt(expression=expr)
341
+
342
+ # ------------------------------------------------------------------
343
+ # Expression parsing (precedence climbing)
344
+ # ------------------------------------------------------------------
345
+
346
+ def _parse_expression(self) -> Any:
347
+ return self._parse_ternary()
348
+
349
+ def _parse_ternary(self) -> Any:
350
+ expr = self._parse_or()
351
+ if self._kw("when"):
352
+ condition = self._parse_or()
353
+ self._expect(TokenType.KEYWORD, "else")
354
+ false_val = self._parse_ternary()
355
+ return TernaryExpr(true_val=expr, condition=condition, false_val=false_val)
356
+ return expr
357
+
358
+ def _parse_or(self) -> Any:
359
+ left = self._parse_and()
360
+ while self._kw("or"):
361
+ left = BinaryOp(left, "or", self._parse_and())
362
+ return left
363
+
364
+ def _parse_and(self) -> Any:
365
+ left = self._parse_not()
366
+ while self._kw("and"):
367
+ left = BinaryOp(left, "and", self._parse_not())
368
+ return left
369
+
370
+ def _parse_not(self) -> Any:
371
+ if self._kw("not"):
372
+ return UnaryOp("not", self._parse_not())
373
+ return self._parse_comparison()
374
+
375
+ def _parse_comparison(self) -> Any:
376
+ left = self._parse_range()
377
+ _comp = {
378
+ TokenType.EQUAL, TokenType.NOT_EQUAL,
379
+ TokenType.LESS, TokenType.GREATER,
380
+ TokenType.LESS_EQUAL, TokenType.GREATER_EQUAL,
381
+ }
382
+ while True:
383
+ if self._peek().type in _comp:
384
+ op = self._advance()
385
+ left = BinaryOp(left, op.value, self._parse_range())
386
+ elif self._peek().type == TokenType.KEYWORD and self._peek().value in ("is", "in", "has"):
387
+ op = self._advance()
388
+ left = BinaryOp(left, op.value, self._parse_range())
389
+ else:
390
+ break
391
+ return left
392
+
393
+ def _parse_range(self) -> Any:
394
+ left = self._parse_addition()
395
+ if self._match(TokenType.DOTDOT_EQUAL):
396
+ right = self._parse_addition()
397
+ return RangeExpr(start=left, end=right, inclusive=True)
398
+ if self._match(TokenType.DOTDOT):
399
+ right = self._parse_addition()
400
+ return RangeExpr(start=left, end=right, inclusive=False)
401
+ return left
402
+
403
+ def _parse_addition(self) -> Any:
404
+ left = self._parse_multiplication()
405
+ while self._peek().type in (TokenType.PLUS, TokenType.MINUS):
406
+ op = self._advance()
407
+ left = BinaryOp(left, op.value, self._parse_multiplication())
408
+ return left
409
+
410
+ def _parse_multiplication(self) -> Any:
411
+ left = self._parse_unary()
412
+ while self._peek().type in (TokenType.STAR, TokenType.SLASH, TokenType.DOUBLE_SLASH, TokenType.PERCENT):
413
+ op = self._advance()
414
+ left = BinaryOp(left, op.value, self._parse_unary())
415
+ return left
416
+
417
+ def _parse_unary(self) -> Any:
418
+ if self._peek().type in (TokenType.MINUS, TokenType.PLUS):
419
+ op = self._advance()
420
+ return UnaryOp(op.value, self._parse_unary())
421
+ return self._parse_power()
422
+
423
+ def _parse_power(self) -> Any:
424
+ base = self._parse_call()
425
+ if self._match(TokenType.POWER):
426
+ return BinaryOp(base, "**", self._parse_unary())
427
+ return base
428
+
429
+ def _parse_call(self) -> Any:
430
+ expr = self._parse_primary()
431
+ while True:
432
+ if self._match(TokenType.LPAREN):
433
+ args: list[Any] = []
434
+ if self._peek().type != TokenType.RPAREN:
435
+ args.append(self._parse_expression())
436
+ while self._match(TokenType.COMMA):
437
+ args.append(self._parse_expression())
438
+ self._expect(TokenType.RPAREN)
439
+ expr = CallExpr(callee=expr, args=args)
440
+ elif self._match(TokenType.LBRACKET):
441
+ idx = self._parse_expression()
442
+ self._expect(TokenType.RBRACKET)
443
+ expr = IndexExpr(obj=expr, index=idx)
444
+ elif self._match(TokenType.DOT):
445
+ member = self._expect(TokenType.IDENTIFIER).value
446
+ expr = MemberExpr(obj=expr, member=member)
447
+ elif self._match(TokenType.PIPE):
448
+ func = self._parse_primary()
449
+ expr = CallExpr(callee=func, args=[expr])
450
+ else:
451
+ break
452
+ return expr
453
+
454
+ def _parse_primary(self) -> Any:
455
+ tok = self._peek()
456
+
457
+ if tok.type == TokenType.NUMBER_INT:
458
+ self._advance()
459
+ return NumberLit(value=int(tok.value, 0))
460
+
461
+ if tok.type == TokenType.NUMBER_FLOAT:
462
+ self._advance()
463
+ return NumberLit(value=float(tok.value))
464
+
465
+ if tok.type == TokenType.STRING:
466
+ self._advance()
467
+ return StringLit(value=tok.value)
468
+
469
+ if tok.type == TokenType.FSTRING:
470
+ self._advance()
471
+ return FStringLit(raw=tok.value)
472
+
473
+ if tok.type == TokenType.BOOL_TRUE:
474
+ self._advance()
475
+ return BoolLit(value=True)
476
+
477
+ if tok.type == TokenType.BOOL_FALSE:
478
+ self._advance()
479
+ return BoolLit(value=False)
480
+
481
+ if tok.type == TokenType.NONE:
482
+ self._advance()
483
+ return NoneLit()
484
+
485
+ if tok.type == TokenType.IDENTIFIER:
486
+ self._advance()
487
+ return Identifier(name=tok.value)
488
+
489
+ # ask / ?
490
+ if tok.type == TokenType.KEYWORD and tok.value == "ask":
491
+ self._advance()
492
+ return AskExpr(prompt=self._parse_expression())
493
+ if tok.type == TokenType.QUESTION:
494
+ self._advance()
495
+ return AskExpr(prompt=self._parse_expression())
496
+
497
+ # new ClassName(args)
498
+ if tok.type == TokenType.KEYWORD and tok.value == "new":
499
+ self._advance()
500
+ cls_name = self._expect(TokenType.IDENTIFIER).value
501
+ self._expect(TokenType.LPAREN)
502
+ args: list[Any] = []
503
+ if self._peek().type != TokenType.RPAREN:
504
+ args.append(self._parse_expression())
505
+ while self._match(TokenType.COMMA):
506
+ args.append(self._parse_expression())
507
+ self._expect(TokenType.RPAREN)
508
+ return CallExpr(callee=Identifier(cls_name), args=args)
509
+
510
+ # self
511
+ if tok.type == TokenType.KEYWORD and tok.value == "self":
512
+ self._advance()
513
+ return Identifier(name="self")
514
+
515
+ # super
516
+ if tok.type == TokenType.KEYWORD and tok.value == "super":
517
+ self._advance()
518
+ return Identifier(name="super")
519
+
520
+ # typeof(x)
521
+ if tok.type == TokenType.KEYWORD and tok.value == "typeof":
522
+ self._advance()
523
+ self._expect(TokenType.LPAREN)
524
+ arg = self._parse_expression()
525
+ self._expect(TokenType.RPAREN)
526
+ return CallExpr(callee=Identifier("typeof"), args=[arg])
527
+
528
+ # List literal
529
+ if tok.type == TokenType.LBRACKET:
530
+ return self._parse_list_literal()
531
+
532
+ # Map literal
533
+ if tok.type == TokenType.LBRACE:
534
+ return self._parse_map_literal()
535
+
536
+ # Grouped expression or lambda
537
+ if tok.type == TokenType.LPAREN:
538
+ return self._parse_grouped_or_lambda()
539
+
540
+ # Wildcard _ (used in match/case)
541
+ if tok.type == TokenType.IDENTIFIER and tok.value == "_":
542
+ self._advance()
543
+ return Identifier(name="_")
544
+
545
+ raise ParseError(
546
+ f"Unexpected token: '{tok.value}' ({tok.type.name})",
547
+ line=tok.line, column=tok.column,
548
+ )
549
+
550
+ # --- composite primaries ---
551
+
552
+ def _parse_list_literal(self) -> ListLit:
553
+ self._advance() # [
554
+ elements: list[Any] = []
555
+ self._skip_nl()
556
+ if self._peek().type != TokenType.RBRACKET:
557
+ elements.append(self._parse_expression())
558
+ while self._match(TokenType.COMMA):
559
+ self._skip_nl()
560
+ if self._peek().type == TokenType.RBRACKET:
561
+ break
562
+ elements.append(self._parse_expression())
563
+ self._skip_nl()
564
+ self._expect(TokenType.RBRACKET)
565
+ return ListLit(elements=elements)
566
+
567
+ def _parse_map_literal(self) -> MapLit:
568
+ self._advance() # {
569
+ entries: list[tuple[Any, Any]] = []
570
+ self._skip_nl()
571
+ if self._peek().type != TokenType.RBRACE:
572
+ entries.append(self._parse_map_entry())
573
+ while self._match(TokenType.COMMA):
574
+ self._skip_nl()
575
+ if self._peek().type == TokenType.RBRACE:
576
+ break
577
+ entries.append(self._parse_map_entry())
578
+ self._skip_nl()
579
+ self._expect(TokenType.RBRACE)
580
+ return MapLit(entries=entries)
581
+
582
+ def _parse_map_entry(self) -> tuple[Any, Any]:
583
+ self._skip_nl()
584
+ # Allow identifier as shorthand key {name: "Alice"}
585
+ if self._peek().type == TokenType.IDENTIFIER and self.tokens[self.pos + 1].type == TokenType.COLON:
586
+ key = StringLit(value=self._advance().value)
587
+ else:
588
+ key = self._parse_expression()
589
+ self._expect(TokenType.COLON)
590
+ value = self._parse_expression()
591
+ return (key, value)
592
+
593
+ def _parse_grouped_or_lambda(self) -> Any:
594
+ self._advance() # (
595
+
596
+ # () => expr
597
+ if self._peek().type == TokenType.RPAREN:
598
+ self._advance()
599
+ if self._match(TokenType.ARROW):
600
+ body = self._parse_expression()
601
+ return LambdaExpr(params=[], body=body)
602
+ # empty parens — treat as none? shouldn't happen, error
603
+ raise ParseError("Empty parentheses", line=self._peek().line, column=self._peek().column)
604
+
605
+ # Try single-expression group first; peek ahead for comma or arrow
606
+ save = self.pos
607
+ try:
608
+ expr = self._parse_expression()
609
+
610
+ # (expr) — grouped
611
+ if self._match(TokenType.RPAREN):
612
+ # Check for => after )
613
+ if self._match(TokenType.ARROW):
614
+ if isinstance(expr, Identifier):
615
+ body = self._parse_expression()
616
+ return LambdaExpr(params=[Param(name=expr.name)], body=body)
617
+ return expr
618
+
619
+ # (id, id, …) => expr — lambda
620
+ if self._peek().type == TokenType.COMMA and isinstance(expr, Identifier):
621
+ params = [Param(name=expr.name)]
622
+ while self._match(TokenType.COMMA):
623
+ p_name = self._expect(TokenType.IDENTIFIER).value
624
+ params.append(Param(name=p_name))
625
+ self._expect(TokenType.RPAREN)
626
+ self._expect(TokenType.ARROW)
627
+ body = self._parse_expression()
628
+ return LambdaExpr(params=params, body=body)
629
+
630
+ # Fallback: just a grouped expression
631
+ self._expect(TokenType.RPAREN)
632
+ return expr
633
+
634
+ except ParseError:
635
+ # Restore and try harder
636
+ self.pos = save
637
+ raise