multilingualprogramming 0.2.0__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.
Files changed (61) hide show
  1. multilingualprogramming/__init__.py +74 -0
  2. multilingualprogramming/__main__.py +194 -0
  3. multilingualprogramming/codegen/__init__.py +12 -0
  4. multilingualprogramming/codegen/executor.py +215 -0
  5. multilingualprogramming/codegen/python_generator.py +592 -0
  6. multilingualprogramming/codegen/repl.py +489 -0
  7. multilingualprogramming/codegen/runtime_builtins.py +308 -0
  8. multilingualprogramming/core/__init__.py +12 -0
  9. multilingualprogramming/core/ir.py +29 -0
  10. multilingualprogramming/core/lowering.py +24 -0
  11. multilingualprogramming/datetime/__init__.py +11 -0
  12. multilingualprogramming/datetime/date_parser.py +190 -0
  13. multilingualprogramming/datetime/mp_date.py +210 -0
  14. multilingualprogramming/datetime/mp_datetime.py +153 -0
  15. multilingualprogramming/datetime/mp_time.py +147 -0
  16. multilingualprogramming/datetime/resource_loader.py +18 -0
  17. multilingualprogramming/exceptions.py +158 -0
  18. multilingualprogramming/imports.py +150 -0
  19. multilingualprogramming/keyword/__init__.py +13 -0
  20. multilingualprogramming/keyword/keyword_registry.py +249 -0
  21. multilingualprogramming/keyword/keyword_validator.py +59 -0
  22. multilingualprogramming/keyword/language_pack_validator.py +110 -0
  23. multilingualprogramming/lexer/__init__.py +11 -0
  24. multilingualprogramming/lexer/lexer.py +570 -0
  25. multilingualprogramming/lexer/source_reader.py +91 -0
  26. multilingualprogramming/lexer/token.py +54 -0
  27. multilingualprogramming/lexer/token_types.py +38 -0
  28. multilingualprogramming/numeral/__init__.py +11 -0
  29. multilingualprogramming/numeral/abstract_numeral.py +232 -0
  30. multilingualprogramming/numeral/complex_numeral.py +190 -0
  31. multilingualprogramming/numeral/fraction_numeral.py +165 -0
  32. multilingualprogramming/numeral/mp_numeral.py +243 -0
  33. multilingualprogramming/numeral/numeral_converter.py +151 -0
  34. multilingualprogramming/numeral/roman_numeral.py +301 -0
  35. multilingualprogramming/numeral/unicode_numeral.py +292 -0
  36. multilingualprogramming/parser/__init__.py +28 -0
  37. multilingualprogramming/parser/ast_nodes.py +459 -0
  38. multilingualprogramming/parser/ast_printer.py +677 -0
  39. multilingualprogramming/parser/error_messages.py +75 -0
  40. multilingualprogramming/parser/parser.py +1796 -0
  41. multilingualprogramming/parser/semantic_analyzer.py +689 -0
  42. multilingualprogramming/parser/surface_normalizer.py +282 -0
  43. multilingualprogramming/resources/datetime/eras.json +23 -0
  44. multilingualprogramming/resources/datetime/formats.json +32 -0
  45. multilingualprogramming/resources/datetime/months.json +150 -0
  46. multilingualprogramming/resources/datetime/weekdays.json +90 -0
  47. multilingualprogramming/resources/parser/error_messages.json +310 -0
  48. multilingualprogramming/resources/repl/commands.json +636 -0
  49. multilingualprogramming/resources/usm/builtins_aliases.json +731 -0
  50. multilingualprogramming/resources/usm/keywords.json +1063 -0
  51. multilingualprogramming/resources/usm/operators.json +532 -0
  52. multilingualprogramming/resources/usm/schema.json +34 -0
  53. multilingualprogramming/resources/usm/surface_patterns.json +1523 -0
  54. multilingualprogramming/unicode_string.py +140 -0
  55. multilingualprogramming/version.py +9 -0
  56. multilingualprogramming-0.2.0.dist-info/METADATA +350 -0
  57. multilingualprogramming-0.2.0.dist-info/RECORD +61 -0
  58. multilingualprogramming-0.2.0.dist-info/WHEEL +5 -0
  59. multilingualprogramming-0.2.0.dist-info/entry_points.txt +3 -0
  60. multilingualprogramming-0.2.0.dist-info/licenses/LICENSE +674 -0
  61. multilingualprogramming-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1796 @@
1
+ #
2
+ # SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
3
+ #
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ #
6
+
7
+ """Recursive-descent parser for the multilingual programming language."""
8
+
9
+ from typing import NoReturn
10
+
11
+ from multilingualprogramming.lexer.lexer import Lexer
12
+ from multilingualprogramming.lexer.token_types import TokenType
13
+ from multilingualprogramming.parser.ast_nodes import (
14
+ Program, NumeralLiteral, StringLiteral, DateLiteral,
15
+ BooleanLiteral, NoneLiteral, ListLiteral, DictLiteral, SetLiteral,
16
+ Identifier, BinaryOp, UnaryOp, BooleanOp, CompareOp,
17
+ CallExpr, AttributeAccess, IndexAccess, ConditionalExpr,
18
+ LambdaExpr, YieldExpr, AwaitExpr, NamedExpr,
19
+ VariableDeclaration, Assignment, AnnAssignment, ExpressionStatement,
20
+ PassStatement, ReturnStatement, BreakStatement, ContinueStatement,
21
+ RaiseStatement, DelStatement, GlobalStatement, LocalStatement, YieldStatement,
22
+ IfStatement, WhileLoop, ForLoop, FunctionDef, ClassDef,
23
+ TryStatement, ExceptHandler, MatchStatement, CaseClause,
24
+ WithStatement, ImportStatement, FromImportStatement,
25
+ SliceExpr, Parameter, StarredExpr, TupleLiteral,
26
+ ComprehensionClause,
27
+ ListComprehension, DictComprehension, GeneratorExpr, SetComprehension,
28
+ FStringLiteral, AssertStatement, ChainedAssignment, DictUnpackEntry,
29
+ )
30
+ from multilingualprogramming.parser.error_messages import ErrorMessageRegistry
31
+ from multilingualprogramming.parser.surface_normalizer import (
32
+ normalize_surface_tokens,
33
+ )
34
+ from multilingualprogramming.exceptions import ParseError
35
+
36
+ # Concepts that begin compound statements
37
+ _COMPOUND_CONCEPTS = {
38
+ "COND_IF", "LOOP_WHILE", "LOOP_FOR", "FUNC_DEF", "CLASS_DEF",
39
+ "TRY", "MATCH", "WITH",
40
+ }
41
+
42
+ # Concepts that begin simple keyword statements
43
+ _SIMPLE_CONCEPTS = {
44
+ "LET", "CONST", "RETURN", "YIELD", "RAISE",
45
+ "LOOP_BREAK", "LOOP_CONTINUE", "PASS",
46
+ "GLOBAL", "LOCAL", "NONLOCAL", "DEL", "IMPORT", "FROM", "ASSERT",
47
+ }
48
+
49
+ # Concepts treated as identifiers when appearing in expressions
50
+ _CALLABLE_CONCEPTS = {"PRINT", "INPUT"}
51
+ _TYPE_CONCEPTS = {
52
+ "TYPE_INT", "TYPE_FLOAT", "TYPE_STR",
53
+ "TYPE_BOOL", "TYPE_LIST", "TYPE_DICT",
54
+ }
55
+ _TYPE_CONCEPT_TO_PYTHON = {
56
+ "TYPE_INT": "int",
57
+ "TYPE_FLOAT": "float",
58
+ "TYPE_STR": "str",
59
+ "TYPE_BOOL": "bool",
60
+ "TYPE_LIST": "list",
61
+ "TYPE_DICT": "dict",
62
+ }
63
+
64
+ # Augmented assignment operators
65
+ _AUGMENTED_OPS = {
66
+ "+=", "-=", "*=", "/=",
67
+ "**=", "//=", "%=", "&=", "|=", "^=", "<<=", ">>=",
68
+ }
69
+
70
+ # Comparison operators
71
+ _COMPARISON_OPS = {"==", "!=", "<", ">", "<=", ">="}
72
+
73
+
74
+ class Parser:
75
+ """
76
+ Recursive-descent parser for the multilingual programming language.
77
+
78
+ Consumes a list[Token] from the Lexer and produces an AST.
79
+ Dispatches on token.concept for language-agnostic parsing.
80
+ """
81
+
82
+ def __init__(self, tokens, source_language=None):
83
+ self.source_language = source_language or "en"
84
+ self.tokens = normalize_surface_tokens(tokens, self.source_language)
85
+ self.pos = 0
86
+ self._error_registry = ErrorMessageRegistry()
87
+
88
+ # ------------------------------------------------------------------
89
+ # Token navigation
90
+ # ------------------------------------------------------------------
91
+
92
+ def _current(self):
93
+ """Return current token without advancing."""
94
+ if self.pos < len(self.tokens):
95
+ return self.tokens[self.pos]
96
+ return self.tokens[-1] # EOF
97
+
98
+ def _advance(self):
99
+ """Consume and return current token."""
100
+ token = self._current()
101
+ if token.type != TokenType.EOF:
102
+ self.pos += 1
103
+ return token
104
+
105
+ def _match_type(self, token_type):
106
+ """Check if current token matches the given type."""
107
+ return self._current().type == token_type
108
+
109
+ def _match_concept(self, concept):
110
+ """Check if current token is a KEYWORD with the given concept."""
111
+ tok = self._current()
112
+ return tok.type == TokenType.KEYWORD and tok.concept == concept
113
+
114
+ def _peek_concept(self, concept):
115
+ """Check if next token (pos+1) is a KEYWORD with the given concept."""
116
+ idx = self.pos + 1
117
+ if idx < len(self.tokens):
118
+ tok = self.tokens[idx]
119
+ return tok.type == TokenType.KEYWORD and tok.concept == concept
120
+ return False
121
+
122
+ def _match_operator(self, op):
123
+ """Check if current token is an OPERATOR with the given value."""
124
+ tok = self._current()
125
+ return tok.type == TokenType.OPERATOR and tok.value == op
126
+
127
+ def _match_delimiter(self, delim):
128
+ """Check if current token is a DELIMITER with the given value."""
129
+ tok = self._current()
130
+ return tok.type == TokenType.DELIMITER and tok.value == delim
131
+
132
+ def _expect_type(self, token_type):
133
+ """Consume if type matches; raise ParseError otherwise."""
134
+ tok = self._current()
135
+ if tok.type == token_type:
136
+ return self._advance()
137
+ self._error(
138
+ "MISMATCHED_DELIMITER",
139
+ tok,
140
+ expected=token_type.name,
141
+ actual=tok.type.name,
142
+ )
143
+
144
+ def _expect_concept(self, concept):
145
+ """Consume if KEYWORD with given concept; raise otherwise."""
146
+ tok = self._current()
147
+ if tok.type == TokenType.KEYWORD and tok.concept == concept:
148
+ return self._advance()
149
+ self._error(
150
+ "UNEXPECTED_TOKEN",
151
+ tok,
152
+ token=tok.value,
153
+ )
154
+
155
+ def _expect_operator(self, op):
156
+ """Consume if OPERATOR with given value; raise otherwise."""
157
+ tok = self._current()
158
+ if tok.type == TokenType.OPERATOR and tok.value == op:
159
+ return self._advance()
160
+ self._error(
161
+ "MISMATCHED_DELIMITER",
162
+ tok,
163
+ expected=op,
164
+ actual=tok.value,
165
+ )
166
+
167
+ def _expect_delimiter(self, delim):
168
+ """Consume if DELIMITER with given value; raise otherwise."""
169
+ tok = self._current()
170
+ if tok.type == TokenType.DELIMITER and tok.value == delim:
171
+ return self._advance()
172
+ self._error(
173
+ "MISMATCHED_DELIMITER",
174
+ tok,
175
+ expected=delim,
176
+ actual=tok.value,
177
+ )
178
+
179
+ def _expect_identifier(self):
180
+ """Consume an IDENTIFIER token; raise otherwise."""
181
+ tok = self._current()
182
+ if tok.type == TokenType.IDENTIFIER:
183
+ return self._advance()
184
+ # Allow keyword tokens that are also valid callable/type names
185
+ # to be used in identifier positions (e.g. French: "liste").
186
+ if tok.type == TokenType.KEYWORD and tok.concept in (
187
+ _CALLABLE_CONCEPTS | _TYPE_CONCEPTS
188
+ ):
189
+ return self._advance()
190
+ self._error("EXPECTED_IDENTIFIER", tok, token=tok.value)
191
+
192
+ def _at_end(self):
193
+ """Check if current token is EOF."""
194
+ return self._current().type == TokenType.EOF
195
+
196
+ def _skip_newlines(self):
197
+ """Skip NEWLINE and COMMENT tokens."""
198
+ while self._current().type in (TokenType.NEWLINE, TokenType.COMMENT):
199
+ self._advance()
200
+
201
+ def _error(self, message_key, err_token, **kwargs) -> NoReturn:
202
+ """Raise a ParseError with a multilingual message."""
203
+ kwargs.setdefault("line", err_token.line)
204
+ kwargs.setdefault("column", err_token.column)
205
+ msg = self._error_registry.format(
206
+ message_key, self.source_language, **kwargs
207
+ )
208
+ raise ParseError(msg, err_token.line, err_token.column)
209
+
210
+ def parse_expression_fragment(self):
211
+ """Parse and return a single expression from the current token stream."""
212
+ return self._parse_expression()
213
+
214
+ # ------------------------------------------------------------------
215
+ # Top-level entry point
216
+ # ------------------------------------------------------------------
217
+
218
+ def parse(self):
219
+ """Parse the token stream into a Program AST."""
220
+ self._skip_newlines()
221
+ body = []
222
+ while not self._at_end():
223
+ stmt = self._parse_statement()
224
+ body.append(stmt)
225
+ self._skip_newlines()
226
+ line = self.tokens[0].line if self.tokens else 0
227
+ col = self.tokens[0].column if self.tokens else 0
228
+ return Program(body, line, col)
229
+
230
+ # ------------------------------------------------------------------
231
+ # Statement parsing
232
+ # ------------------------------------------------------------------
233
+
234
+ def _parse_statement(self):
235
+ """Parse a single statement."""
236
+ self._skip_newlines()
237
+ tok = self._current()
238
+
239
+ # Decorators: @expr before def/class
240
+ if self._match_delimiter("@"):
241
+ return self._parse_decorated()
242
+
243
+ if tok.type == TokenType.KEYWORD and tok.concept == "ASYNC":
244
+ return self._parse_async_statement()
245
+
246
+ if tok.type == TokenType.KEYWORD and tok.concept:
247
+ concept = tok.concept
248
+ if concept in _COMPOUND_CONCEPTS:
249
+ return self._parse_compound_statement(concept)
250
+ if concept in _SIMPLE_CONCEPTS:
251
+ return self._parse_simple_statement(concept)
252
+
253
+ return self._parse_assignment_or_expression()
254
+
255
+ def _parse_async_statement(self):
256
+ """Parse async-prefixed compound statements."""
257
+ tok = self._advance() # consume ASYNC
258
+ if self._match_concept("FUNC_DEF"):
259
+ return self._parse_function_def(is_async=True, async_tok=tok)
260
+ if self._match_concept("LOOP_FOR"):
261
+ return self._parse_for_loop(is_async=True, async_tok=tok)
262
+ if self._match_concept("WITH"):
263
+ return self._parse_with_statement(is_async=True, async_tok=tok)
264
+ self._error("UNEXPECTED_TOKEN", self._current(),
265
+ token=self._current().value)
266
+
267
+ def _parse_decorated(self):
268
+ """Parse decorated function or class definition."""
269
+ decorators = []
270
+ while self._match_delimiter("@"):
271
+ self._advance() # consume @
272
+ dec_expr = self._parse_expression()
273
+ decorators.append(dec_expr)
274
+ self._skip_newlines()
275
+
276
+ # The next statement must be a function or class def
277
+ tok = self._current()
278
+ if tok.type == TokenType.KEYWORD and tok.concept == "FUNC_DEF":
279
+ node = self._parse_function_def()
280
+ node.decorators = decorators
281
+ return node
282
+ if tok.type == TokenType.KEYWORD and tok.concept == "ASYNC":
283
+ node = self._parse_async_statement()
284
+ if isinstance(node, FunctionDef):
285
+ node.decorators = decorators
286
+ return node
287
+ if tok.type == TokenType.KEYWORD and tok.concept == "CLASS_DEF":
288
+ node = self._parse_class_def()
289
+ node.decorators = decorators
290
+ return node
291
+
292
+ self._error("UNEXPECTED_TOKEN", tok, token=tok.value)
293
+
294
+ def _parse_compound_statement(self, concept):
295
+ """Parse a compound (block) statement."""
296
+ if concept == "COND_IF":
297
+ return self._parse_if_statement()
298
+ if concept == "LOOP_WHILE":
299
+ return self._parse_while_loop()
300
+ if concept == "LOOP_FOR":
301
+ return self._parse_for_loop()
302
+ if concept == "FUNC_DEF":
303
+ return self._parse_function_def()
304
+ if concept == "CLASS_DEF":
305
+ return self._parse_class_def()
306
+ if concept == "TRY":
307
+ return self._parse_try_statement()
308
+ if concept == "MATCH":
309
+ return self._parse_match_statement()
310
+ if concept == "WITH":
311
+ return self._parse_with_statement()
312
+ self._error("UNEXPECTED_TOKEN", self._current(),
313
+ token=self._current().value)
314
+
315
+ def _parse_simple_statement(self, concept):
316
+ """Parse a simple keyword statement."""
317
+ if concept == "LET":
318
+ return self._parse_let_declaration()
319
+ if concept == "CONST":
320
+ return self._parse_const_declaration()
321
+ if concept == "RETURN":
322
+ return self._parse_return_statement()
323
+ if concept == "YIELD":
324
+ return self._parse_yield_statement()
325
+ if concept == "RAISE":
326
+ return self._parse_raise_statement()
327
+ if concept == "DEL":
328
+ return self._parse_del_statement()
329
+ if concept == "LOOP_BREAK":
330
+ return self._parse_break_statement()
331
+ if concept == "LOOP_CONTINUE":
332
+ return self._parse_continue_statement()
333
+ if concept == "PASS":
334
+ return self._parse_pass_statement()
335
+ if concept == "GLOBAL":
336
+ return self._parse_global_statement()
337
+ if concept in {"LOCAL", "NONLOCAL"}:
338
+ return self._parse_nonlocal_statement()
339
+ if concept == "IMPORT":
340
+ return self._parse_import_statement()
341
+ if concept == "FROM":
342
+ return self._parse_from_import_statement()
343
+ if concept == "ASSERT":
344
+ return self._parse_assert_statement()
345
+ self._error("UNEXPECTED_TOKEN", self._current(),
346
+ token=self._current().value)
347
+
348
+ # ------------------------------------------------------------------
349
+ # Block parsing
350
+ # ------------------------------------------------------------------
351
+
352
+ def _parse_block(self):
353
+ """Parse an indented block: colon NEWLINE INDENT stmts DEDENT."""
354
+ self._expect_delimiter(":")
355
+ self._skip_newlines()
356
+ self._expect_type(TokenType.INDENT)
357
+ self._skip_newlines()
358
+
359
+ body = []
360
+ while not self._at_end() and not self._match_type(TokenType.DEDENT):
361
+ stmt = self._parse_statement()
362
+ body.append(stmt)
363
+ self._skip_newlines()
364
+
365
+ if self._match_type(TokenType.DEDENT):
366
+ self._advance()
367
+
368
+ return body
369
+
370
+ # ------------------------------------------------------------------
371
+ # Variable declarations and assignments
372
+ # ------------------------------------------------------------------
373
+
374
+ def _parse_target_item(self):
375
+ """Parse a single assignment target: identifier or *identifier."""
376
+ if self._match_operator("*"):
377
+ star_tok = self._advance()
378
+ id_tok = self._expect_identifier()
379
+ return StarredExpr(
380
+ Identifier(id_tok.value, line=id_tok.line, column=id_tok.column),
381
+ is_double=False,
382
+ line=star_tok.line, column=star_tok.column
383
+ )
384
+ id_tok = self._expect_identifier()
385
+ return Identifier(id_tok.value, line=id_tok.line, column=id_tok.column)
386
+
387
+ def _parse_let_declaration(self):
388
+ """Parse LET declaration, including tuple/chained assignment forms."""
389
+ tok = self._advance() # consume LET
390
+ target = self._parse_target_item()
391
+
392
+ # Annotated LET: let x: T = value (only for plain identifiers)
393
+ if isinstance(target, Identifier) and self._match_delimiter(":"):
394
+ self._advance()
395
+ annotation = self._parse_annotation_expression()
396
+ self._expect_operator("=")
397
+ value = self._parse_expression()
398
+ return AnnAssignment(
399
+ target, annotation, value,
400
+ line=tok.line, column=tok.column
401
+ )
402
+
403
+ if self._match_delimiter(","):
404
+ elements = [target]
405
+ while self._match_delimiter(","):
406
+ self._advance()
407
+ # Stop if we hit = after comma (trailing comma)
408
+ if self._match_operator("="):
409
+ break
410
+ elements.append(self._parse_target_item())
411
+ target = TupleLiteral(elements, line=tok.line, column=tok.column)
412
+
413
+ self._expect_operator("=")
414
+ value = self._parse_expression()
415
+
416
+ # Tuple on right side: let a, b = x, y
417
+ if self._match_delimiter(","):
418
+ right_elements = [value]
419
+ while self._match_delimiter(","):
420
+ self._advance()
421
+ if self._at_end() or self._match_type(TokenType.NEWLINE):
422
+ break
423
+ right_elements.append(self._parse_expression())
424
+ value = TupleLiteral(right_elements, line=tok.line, column=tok.column)
425
+
426
+ # Chained assignment: let a = b = c = 7
427
+ if self._match_operator("="):
428
+ targets = [target, value]
429
+ while self._match_operator("="):
430
+ self._advance()
431
+ value = self._parse_expression()
432
+ targets.append(value)
433
+ final_value = targets.pop()
434
+ return ChainedAssignment(targets, final_value,
435
+ line=tok.line, column=tok.column)
436
+
437
+ if isinstance(target, Identifier):
438
+ return VariableDeclaration(
439
+ target.name, value, is_const=False,
440
+ line=tok.line, column=tok.column
441
+ )
442
+ return Assignment(target, value, op="=",
443
+ line=tok.line, column=tok.column)
444
+
445
+ def _parse_const_declaration(self):
446
+ """Parse: CONST name = expression."""
447
+ tok = self._advance() # consume CONST
448
+ name_tok = self._expect_identifier()
449
+ self._expect_operator("=")
450
+ value = self._parse_expression()
451
+ return VariableDeclaration(
452
+ name_tok.value, value, is_const=True,
453
+ line=tok.line, column=tok.column
454
+ )
455
+
456
+ def _parse_assignment_or_expression(self):
457
+ """Parse assignment or expression statement."""
458
+ # Check for starred target at statement start: *rest, a = ...
459
+ if self._match_operator("*"):
460
+ star_tok = self._advance()
461
+ id_tok = self._expect_identifier()
462
+ expr = StarredExpr(
463
+ Identifier(id_tok.value, line=id_tok.line, column=id_tok.column),
464
+ is_double=False,
465
+ line=star_tok.line, column=star_tok.column
466
+ )
467
+ else:
468
+ expr = self._parse_expression()
469
+
470
+ # Annotated assignment: name: type [= value]
471
+ if isinstance(expr, Identifier) and self._match_delimiter(":"):
472
+ tok = self._advance() # consume :
473
+ annotation = self._parse_annotation_expression()
474
+ value = None
475
+ if self._match_operator("="):
476
+ self._advance()
477
+ value = self._parse_expression()
478
+ return AnnAssignment(
479
+ expr, annotation, value,
480
+ line=tok.line, column=tok.column
481
+ )
482
+
483
+ # Check for comma (tuple unpacking: a, b = ... or a, *rest = ...)
484
+ if self._match_delimiter(","):
485
+ elements = [expr]
486
+ while self._match_delimiter(","):
487
+ self._advance()
488
+ # Stop if we hit = after comma (trailing comma)
489
+ if self._current().type == TokenType.OPERATOR \
490
+ and self._current().value == "=":
491
+ break
492
+ # Support starred element: a, *rest = ...
493
+ if self._match_operator("*"):
494
+ star_tok = self._advance()
495
+ id_tok = self._expect_identifier()
496
+ elements.append(StarredExpr(
497
+ Identifier(id_tok.value,
498
+ line=id_tok.line, column=id_tok.column),
499
+ is_double=False,
500
+ line=star_tok.line, column=star_tok.column
501
+ ))
502
+ else:
503
+ elements.append(self._parse_expression())
504
+ expr = TupleLiteral(elements,
505
+ line=expr.line, column=expr.column)
506
+
507
+ # Check for assignment operators
508
+ tok = self._current()
509
+ if tok.type == TokenType.OPERATOR and tok.value == "=":
510
+ self._advance()
511
+ value = self._parse_expression()
512
+ # Check for tuple on the right side too
513
+ if self._match_delimiter(","):
514
+ right_elements = [value]
515
+ while self._match_delimiter(","):
516
+ self._advance()
517
+ if self._at_end() or self._match_type(TokenType.NEWLINE):
518
+ break
519
+ right_elements.append(self._parse_expression())
520
+ value = TupleLiteral(right_elements,
521
+ line=value.line, column=value.column)
522
+ # Check for chained assignment (a = b = c = 0)
523
+ if self._current().type == TokenType.OPERATOR \
524
+ and self._current().value == "=":
525
+ targets = [expr, value]
526
+ while self._current().type == TokenType.OPERATOR \
527
+ and self._current().value == "=":
528
+ self._advance()
529
+ value = self._parse_expression()
530
+ targets.append(value)
531
+ final_value = targets.pop()
532
+ return ChainedAssignment(
533
+ targets, final_value,
534
+ line=expr.line, column=expr.column
535
+ )
536
+ return Assignment(
537
+ expr, value, op="=",
538
+ line=expr.line, column=expr.column
539
+ )
540
+
541
+ if tok.type == TokenType.OPERATOR and tok.value in _AUGMENTED_OPS:
542
+ op = self._advance().value
543
+ value = self._parse_expression()
544
+ return Assignment(
545
+ expr, value, op=op,
546
+ line=expr.line, column=expr.column
547
+ )
548
+
549
+ return ExpressionStatement(
550
+ expr, line=expr.line, column=expr.column
551
+ )
552
+
553
+ # ------------------------------------------------------------------
554
+ # Control flow
555
+ # ------------------------------------------------------------------
556
+
557
+ def _parse_if_statement(self):
558
+ """Parse: IF condition : block [ELIF ...] [ELSE ...]."""
559
+ tok = self._advance() # consume IF
560
+ condition = self._parse_expression()
561
+ body = self._parse_block()
562
+ self._skip_newlines()
563
+
564
+ elif_clauses = []
565
+ while self._match_concept("COND_ELIF"):
566
+ self._advance()
567
+ elif_cond = self._parse_expression()
568
+ elif_body = self._parse_block()
569
+ elif_clauses.append((elif_cond, elif_body))
570
+ self._skip_newlines()
571
+
572
+ else_body = None
573
+ if self._match_concept("COND_ELSE"):
574
+ self._advance()
575
+ else_body = self._parse_block()
576
+
577
+ return IfStatement(
578
+ condition, body, elif_clauses, else_body,
579
+ line=tok.line, column=tok.column
580
+ )
581
+
582
+ def _parse_while_loop(self):
583
+ """Parse: WHILE condition : block [ELSE : block]."""
584
+ tok = self._advance() # consume WHILE
585
+ condition = self._parse_expression()
586
+ body = self._parse_block()
587
+ self._skip_newlines()
588
+ else_body = None
589
+ if self._match_concept("COND_ELSE"):
590
+ self._advance()
591
+ else_body = self._parse_block()
592
+ return WhileLoop(
593
+ condition, body, else_body=else_body,
594
+ line=tok.line, column=tok.column
595
+ )
596
+
597
+ def _parse_for_loop(self, is_async=False, async_tok=None):
598
+ """Parse: FOR target[, target2, ...] IN iterable : block [ELSE : block]."""
599
+ tok = self._advance() # consume FOR
600
+ target = self._parse_target_item()
601
+ # Support tuple unpacking: for a, b in items
602
+ # and starred unpacking: for a, *rest in items
603
+ if self._match_delimiter(","):
604
+ elements = [target]
605
+ while self._match_delimiter(","):
606
+ self._advance()
607
+ elements.append(self._parse_target_item())
608
+ target = TupleLiteral(elements,
609
+ line=target.line, column=target.column)
610
+ self._expect_concept("IN")
611
+ iterable = self._parse_expression()
612
+ body = self._parse_block()
613
+ self._skip_newlines()
614
+ else_body = None
615
+ if self._match_concept("COND_ELSE"):
616
+ self._advance()
617
+ else_body = self._parse_block()
618
+ line = async_tok.line if async_tok else tok.line
619
+ column = async_tok.column if async_tok else tok.column
620
+ return ForLoop(
621
+ target, iterable, body, is_async=is_async,
622
+ else_body=else_body,
623
+ line=line, column=column
624
+ )
625
+
626
+ def _parse_match_statement(self):
627
+ """Parse: MATCH subject : NEWLINE INDENT (CASE/DEFAULT : block)+ DEDENT."""
628
+ tok = self._advance() # consume MATCH
629
+ subject = self._parse_expression()
630
+ self._expect_delimiter(":")
631
+ self._skip_newlines()
632
+ self._expect_type(TokenType.INDENT)
633
+ self._skip_newlines()
634
+
635
+ cases = []
636
+ while not self._at_end() and not self._match_type(TokenType.DEDENT):
637
+ if self._match_concept("CASE"):
638
+ case_tok = self._advance()
639
+ pattern = self._parse_case_pattern()
640
+ guard = None
641
+ if self._match_concept("COND_IF"):
642
+ self._advance()
643
+ guard = self._parse_expression()
644
+ case_body = self._parse_block()
645
+ cases.append(CaseClause(
646
+ pattern, case_body, is_default=False,
647
+ guard=guard,
648
+ line=case_tok.line, column=case_tok.column
649
+ ))
650
+ elif self._match_concept("DEFAULT"):
651
+ case_tok = self._advance()
652
+ case_body = self._parse_block()
653
+ cases.append(CaseClause(
654
+ None, case_body, is_default=True,
655
+ line=case_tok.line, column=case_tok.column
656
+ ))
657
+ else:
658
+ self._error("UNEXPECTED_TOKEN", self._current(),
659
+ token=self._current().value)
660
+ self._skip_newlines()
661
+
662
+ if self._match_type(TokenType.DEDENT):
663
+ self._advance()
664
+
665
+ return MatchStatement(
666
+ subject, cases,
667
+ line=tok.line, column=tok.column
668
+ )
669
+
670
+ def _parse_case_pattern(self):
671
+ """Parse a case pattern: pattern [| pattern]* [AS name].
672
+
673
+ Uses expression parsing for individual patterns, then handles
674
+ OR patterns (|) and AS binding at the pattern level.
675
+ """
676
+ pattern = self._parse_or_expression()
677
+
678
+ # OR patterns: pattern | pattern | ...
679
+ if self._match_operator("|"):
680
+ patterns = [pattern]
681
+ while self._match_operator("|"):
682
+ self._advance()
683
+ patterns.append(self._parse_or_expression())
684
+ # Emit as BinaryOp chain with | operator for codegen
685
+ pattern = patterns[0]
686
+ for right in patterns[1:]:
687
+ pattern = BinaryOp(
688
+ pattern, "|", right,
689
+ line=pattern.line, column=pattern.column
690
+ )
691
+
692
+ # AS binding: pattern as name
693
+ if self._match_concept("AS"):
694
+ self._advance()
695
+ name_tok = self._expect_identifier()
696
+ # Emit as NamedExpr-like pattern (codegen emits "pattern as name")
697
+ name_node = Identifier(
698
+ name_tok.value,
699
+ line=name_tok.line, column=name_tok.column
700
+ )
701
+ pattern = BinaryOp(
702
+ pattern, " as ", name_node,
703
+ line=pattern.line, column=pattern.column
704
+ )
705
+
706
+ return pattern
707
+
708
+ # ------------------------------------------------------------------
709
+ # Definitions
710
+ # ------------------------------------------------------------------
711
+
712
+ def _parse_function_def(self, is_async=False, async_tok=None):
713
+ """Parse: FUNC_DEF name ( params ) : block."""
714
+ tok = self._advance() # consume FUNC_DEF
715
+ name_tok = self._expect_identifier()
716
+ self._expect_delimiter("(")
717
+
718
+ params = []
719
+ if not self._match_delimiter(")"):
720
+ params.append(self._parse_parameter())
721
+ while self._match_delimiter(","):
722
+ self._advance()
723
+ params.append(self._parse_parameter())
724
+
725
+ self._expect_delimiter(")")
726
+ return_annotation = None
727
+ if self._match_operator("->"):
728
+ self._advance()
729
+ return_annotation = self._parse_annotation_expression()
730
+ body = self._parse_block()
731
+ line = async_tok.line if async_tok else tok.line
732
+ column = async_tok.column if async_tok else tok.column
733
+ return FunctionDef(
734
+ name_tok.value, params, body,
735
+ return_annotation=return_annotation,
736
+ is_async=is_async,
737
+ line=line, column=column
738
+ )
739
+
740
+ def _parse_parameter(self):
741
+ """Parse a single function parameter: [*|**] name [: type] [= default].
742
+
743
+ Also handles bare * (keyword-only separator) and / (positional-only separator).
744
+ """
745
+ is_vararg = False
746
+ is_kwarg = False
747
+
748
+ # Handle / for positional-only parameter separator
749
+ if self._match_operator("/"):
750
+ tok = self._advance()
751
+ return Parameter("/", line=tok.line, column=tok.column)
752
+
753
+ if self._match_operator("**"):
754
+ self._advance()
755
+ is_kwarg = True
756
+ elif self._match_operator("*"):
757
+ tok = self._advance()
758
+ # Bare * (keyword-only separator) vs *name (vararg)
759
+ if self._match_delimiter(",") or self._match_delimiter(")"):
760
+ return Parameter("*", line=tok.line, column=tok.column)
761
+ is_vararg = True
762
+
763
+ name_tok = self._expect_identifier()
764
+ annotation = None
765
+ if self._match_delimiter(":"):
766
+ self._advance()
767
+ annotation = self._parse_annotation_expression()
768
+ default = None
769
+ if self._match_operator("="):
770
+ self._advance()
771
+ default = self._parse_expression()
772
+
773
+ return Parameter(
774
+ name_tok.value, default, is_vararg, is_kwarg, annotation,
775
+ line=name_tok.line, column=name_tok.column
776
+ )
777
+
778
+ def _parse_class_def(self):
779
+ """Parse: CLASS_DEF name [(bases)] : block."""
780
+ tok = self._advance() # consume CLASS_DEF
781
+ name_tok = self._expect_identifier()
782
+
783
+ bases = []
784
+ if self._match_delimiter("("):
785
+ self._advance()
786
+ if not self._match_delimiter(")"):
787
+ bases.append(self._parse_expression())
788
+ while self._match_delimiter(","):
789
+ self._advance()
790
+ bases.append(self._parse_expression())
791
+ self._expect_delimiter(")")
792
+
793
+ body = self._parse_block()
794
+ return ClassDef(
795
+ name_tok.value, bases, body,
796
+ line=tok.line, column=tok.column
797
+ )
798
+
799
+ # ------------------------------------------------------------------
800
+ # Error handling
801
+ # ------------------------------------------------------------------
802
+
803
+ def _parse_try_statement(self):
804
+ """Parse: TRY : block (EXCEPT ...)* [ELSE ...] [FINALLY ...]."""
805
+ tok = self._advance() # consume TRY
806
+ body = self._parse_block()
807
+ self._skip_newlines()
808
+
809
+ handlers = []
810
+ while self._match_concept("EXCEPT"):
811
+ handlers.append(self._parse_except_handler())
812
+ self._skip_newlines()
813
+
814
+ else_body = None
815
+ if self._match_concept("COND_ELSE"):
816
+ if not handlers:
817
+ self._error("UNEXPECTED_TOKEN", self._current(),
818
+ token=self._current().value)
819
+ self._advance()
820
+ else_body = self._parse_block()
821
+ self._skip_newlines()
822
+
823
+ finally_body = None
824
+ if self._match_concept("FINALLY"):
825
+ self._advance()
826
+ finally_body = self._parse_block()
827
+
828
+ return TryStatement(
829
+ body, handlers, else_body=else_body, finally_body=finally_body,
830
+ line=tok.line, column=tok.column
831
+ )
832
+
833
+ def _parse_except_handler(self):
834
+ """Parse: EXCEPT [Type [AS name]] : block."""
835
+ tok = self._advance() # consume EXCEPT
836
+ exc_type = None
837
+ name = None
838
+
839
+ # Check for exception type
840
+ if not self._match_delimiter(":"):
841
+ exc_type_tok = self._expect_identifier()
842
+ exc_type = Identifier(
843
+ exc_type_tok.value,
844
+ line=exc_type_tok.line, column=exc_type_tok.column
845
+ )
846
+ if self._match_concept("AS"):
847
+ self._advance()
848
+ name_tok = self._expect_identifier()
849
+ name = name_tok.value
850
+
851
+ handler_body = self._parse_block()
852
+ return ExceptHandler(
853
+ exc_type, name, handler_body,
854
+ line=tok.line, column=tok.column
855
+ )
856
+
857
+ # ------------------------------------------------------------------
858
+ # With statement
859
+ # ------------------------------------------------------------------
860
+
861
+ def _parse_with_statement(self, is_async=False, async_tok=None):
862
+ """Parse: WITH expression [AS name] (, expression [AS name])* : block."""
863
+ tok = self._advance() # consume WITH
864
+ items = []
865
+ while True:
866
+ context_expr = self._parse_expression()
867
+ name = None
868
+ if self._match_concept("AS"):
869
+ self._advance()
870
+ name_tok = self._expect_identifier()
871
+ name = name_tok.value
872
+ items.append((context_expr, name))
873
+ if not self._match_delimiter(","):
874
+ break
875
+ self._advance()
876
+
877
+ body = self._parse_block()
878
+ line = async_tok.line if async_tok else tok.line
879
+ column = async_tok.column if async_tok else tok.column
880
+ return WithStatement(
881
+ items, body=body, is_async=is_async,
882
+ line=line, column=column
883
+ )
884
+
885
+ # ------------------------------------------------------------------
886
+ # Import
887
+ # ------------------------------------------------------------------
888
+
889
+ def _parse_import_statement(self):
890
+ """Parse: IMPORT module [AS alias]."""
891
+ tok = self._advance() # consume IMPORT
892
+ module_tok = self._expect_identifier()
893
+ module = module_tok.value
894
+
895
+ # Support dotted module names
896
+ while self._match_delimiter("."):
897
+ self._advance()
898
+ next_tok = self._expect_identifier()
899
+ module += "." + next_tok.value
900
+
901
+ alias = None
902
+ if self._match_concept("AS"):
903
+ self._advance()
904
+ alias_tok = self._expect_identifier()
905
+ alias = alias_tok.value
906
+
907
+ return ImportStatement(
908
+ module, alias,
909
+ line=tok.line, column=tok.column
910
+ )
911
+
912
+ def _parse_from_import_statement(self):
913
+ """Parse: FROM module IMPORT name [AS alias], ..."""
914
+ tok = self._advance() # consume FROM
915
+ module_tok = self._expect_identifier()
916
+ module = module_tok.value
917
+
918
+ while self._match_delimiter("."):
919
+ self._advance()
920
+ next_tok = self._expect_identifier()
921
+ module += "." + next_tok.value
922
+
923
+ self._expect_concept("IMPORT")
924
+
925
+ # Handle 'from module import *'
926
+ if self._match_operator("*"):
927
+ self._advance()
928
+ return FromImportStatement(
929
+ module, [("*", None)],
930
+ line=tok.line, column=tok.column
931
+ )
932
+
933
+ names = []
934
+ name_tok = self._expect_identifier()
935
+ alias = None
936
+ if self._match_concept("AS"):
937
+ self._advance()
938
+ alias_tok = self._expect_identifier()
939
+ alias = alias_tok.value
940
+ names.append((name_tok.value, alias))
941
+
942
+ while self._match_delimiter(","):
943
+ self._advance()
944
+ name_tok = self._expect_identifier()
945
+ alias = None
946
+ if self._match_concept("AS"):
947
+ self._advance()
948
+ alias_tok = self._expect_identifier()
949
+ alias = alias_tok.value
950
+ names.append((name_tok.value, alias))
951
+
952
+ return FromImportStatement(
953
+ module, names,
954
+ line=tok.line, column=tok.column
955
+ )
956
+
957
+ # ------------------------------------------------------------------
958
+ # Other simple statements
959
+ # ------------------------------------------------------------------
960
+
961
+ def _parse_return_statement(self):
962
+ """Parse: RETURN [expression]."""
963
+ tok = self._advance() # consume RETURN
964
+ value = None
965
+ if not self._at_end() and not self._match_type(TokenType.NEWLINE) \
966
+ and not self._match_type(TokenType.DEDENT) \
967
+ and not self._match_type(TokenType.EOF):
968
+ value = self._parse_expression()
969
+ return ReturnStatement(value, line=tok.line, column=tok.column)
970
+
971
+ def _parse_yield_statement(self):
972
+ """Parse: YIELD [FROM] [expression]."""
973
+ tok = self._advance() # consume YIELD
974
+ is_from = False
975
+ if self._match_concept("FROM"):
976
+ self._advance()
977
+ is_from = True
978
+ value = None
979
+ if not self._at_end() and not self._match_type(TokenType.NEWLINE) \
980
+ and not self._match_type(TokenType.DEDENT) \
981
+ and not self._match_type(TokenType.EOF):
982
+ value = self._parse_expression()
983
+ return YieldStatement(value, is_from=is_from,
984
+ line=tok.line, column=tok.column)
985
+
986
+ def _parse_raise_statement(self):
987
+ """Parse: RAISE [expression [FROM expression]]."""
988
+ tok = self._advance() # consume RAISE
989
+ value = None
990
+ cause = None
991
+ if not self._at_end() and not self._match_type(TokenType.NEWLINE) \
992
+ and not self._match_type(TokenType.DEDENT) \
993
+ and not self._match_type(TokenType.EOF):
994
+ value = self._parse_expression()
995
+ if self._match_concept("FROM"):
996
+ self._advance()
997
+ cause = self._parse_expression()
998
+ return RaiseStatement(value, cause=cause,
999
+ line=tok.line, column=tok.column)
1000
+
1001
+ def _parse_del_statement(self):
1002
+ """Parse: DEL target [, target]*."""
1003
+ tok = self._advance() # consume DEL
1004
+ target = self._parse_expression()
1005
+ if self._match_delimiter(","):
1006
+ elements = [target]
1007
+ while self._match_delimiter(","):
1008
+ self._advance()
1009
+ if self._at_end() or self._match_type(TokenType.NEWLINE):
1010
+ break
1011
+ elements.append(self._parse_expression())
1012
+ target = TupleLiteral(elements, line=tok.line, column=tok.column)
1013
+ return DelStatement(target, line=tok.line, column=tok.column)
1014
+
1015
+ def _parse_assert_statement(self):
1016
+ """Parse: ASSERT test [, msg]."""
1017
+ tok = self._advance() # consume ASSERT
1018
+ test = self._parse_expression()
1019
+ msg = None
1020
+ if self._match_delimiter(","):
1021
+ self._advance()
1022
+ msg = self._parse_expression()
1023
+ return AssertStatement(test, msg, line=tok.line, column=tok.column)
1024
+
1025
+ def _parse_break_statement(self):
1026
+ """Parse: BREAK."""
1027
+ tok = self._advance()
1028
+ return BreakStatement(line=tok.line, column=tok.column)
1029
+
1030
+ def _parse_continue_statement(self):
1031
+ """Parse: CONTINUE."""
1032
+ tok = self._advance()
1033
+ return ContinueStatement(line=tok.line, column=tok.column)
1034
+
1035
+ def _parse_pass_statement(self):
1036
+ """Parse: PASS."""
1037
+ tok = self._advance()
1038
+ return PassStatement(line=tok.line, column=tok.column)
1039
+
1040
+ def _parse_global_statement(self):
1041
+ """Parse: GLOBAL name, name, ..."""
1042
+ tok = self._advance() # consume GLOBAL
1043
+ names = [self._expect_identifier().value]
1044
+ while self._match_delimiter(","):
1045
+ self._advance()
1046
+ names.append(self._expect_identifier().value)
1047
+ return GlobalStatement(names, line=tok.line, column=tok.column)
1048
+
1049
+ def _parse_nonlocal_statement(self):
1050
+ """Parse: NONLOCAL name, name, ..."""
1051
+ tok = self._advance() # consume NONLOCAL
1052
+ names = [self._expect_identifier().value]
1053
+ while self._match_delimiter(","):
1054
+ self._advance()
1055
+ names.append(self._expect_identifier().value)
1056
+ return LocalStatement(names, line=tok.line, column=tok.column)
1057
+
1058
+ # ------------------------------------------------------------------
1059
+ # Expression parsing (precedence climbing)
1060
+ # ------------------------------------------------------------------
1061
+
1062
+ def _parse_expression(self):
1063
+ """Parse an expression (top level)."""
1064
+ return self._parse_named_expression()
1065
+
1066
+ def _parse_annotation_expression(self):
1067
+ """Parse an annotation expression with localized type keyword mapping."""
1068
+ tok = self._current()
1069
+ if tok.type == TokenType.KEYWORD and tok.concept in _TYPE_CONCEPTS:
1070
+ self._advance()
1071
+ return Identifier(
1072
+ _TYPE_CONCEPT_TO_PYTHON[tok.concept],
1073
+ line=tok.line, column=tok.column
1074
+ )
1075
+ return self._parse_expression()
1076
+
1077
+ def _parse_named_expression(self):
1078
+ """Parse assignment expression: conditional_expr [:= expression]."""
1079
+ left = self._parse_conditional_expression()
1080
+ if self._match_operator(":="):
1081
+ tok = self._advance()
1082
+ if not isinstance(left, Identifier):
1083
+ self._error("UNEXPECTED_TOKEN", tok, token=tok.value)
1084
+ value = self._parse_expression()
1085
+ return NamedExpr(left, value, line=tok.line, column=tok.column)
1086
+ return left
1087
+
1088
+ def _parse_conditional_expression(self):
1089
+ """Parse: or_expr [IF or_expr ELSE conditional_expr]."""
1090
+ true_expr = self._parse_or_expression()
1091
+ if self._match_concept("COND_IF"):
1092
+ tok = self._advance()
1093
+ condition = self._parse_or_expression()
1094
+ if not self._match_concept("COND_ELSE"):
1095
+ self._error("EXPECTED_EXPRESSION", self._current())
1096
+ self._advance()
1097
+ false_expr = self._parse_conditional_expression()
1098
+ return ConditionalExpr(
1099
+ condition, true_expr, false_expr,
1100
+ line=tok.line, column=tok.column
1101
+ )
1102
+ return true_expr
1103
+
1104
+ def _parse_or_expression(self):
1105
+ """Parse: and_expr (OR and_expr)*."""
1106
+ left = self._parse_and_expression()
1107
+ values = [left]
1108
+ while self._match_concept("OR"):
1109
+ self._advance()
1110
+ values.append(self._parse_and_expression())
1111
+ if len(values) == 1:
1112
+ return left
1113
+ return BooleanOp("OR", values, line=left.line, column=left.column)
1114
+
1115
+ def _parse_and_expression(self):
1116
+ """Parse: not_expr (AND not_expr)*."""
1117
+ left = self._parse_not_expression()
1118
+ values = [left]
1119
+ while self._match_concept("AND"):
1120
+ self._advance()
1121
+ values.append(self._parse_not_expression())
1122
+ if len(values) == 1:
1123
+ return left
1124
+ return BooleanOp("AND", values, line=left.line, column=left.column)
1125
+
1126
+ def _parse_not_expression(self):
1127
+ """Parse: NOT not_expr | comparison."""
1128
+ if self._match_concept("NOT"):
1129
+ tok = self._advance()
1130
+ operand = self._parse_not_expression()
1131
+ return UnaryOp("NOT", operand, line=tok.line, column=tok.column)
1132
+ return self._parse_comparison()
1133
+
1134
+ def _parse_comparison(self):
1135
+ """Parse: bitwise_or (comp_op bitwise_or)* (chained).
1136
+
1137
+ Supports standard operators (==, !=, <, >, <=, >=) plus
1138
+ keyword operators: in, not in, is, is not.
1139
+ """
1140
+ left = self._parse_bitwise_or()
1141
+ comparators = []
1142
+ while True:
1143
+ if self._current().type == TokenType.OPERATOR \
1144
+ and self._current().value in _COMPARISON_OPS:
1145
+ op = self._advance().value
1146
+ right = self._parse_bitwise_or()
1147
+ comparators.append((op, right))
1148
+ elif self._match_concept("IN"):
1149
+ self._advance()
1150
+ right = self._parse_bitwise_or()
1151
+ comparators.append(("in", right))
1152
+ elif self._match_concept("NOT") and self._peek_concept("IN"):
1153
+ self._advance() # consume NOT
1154
+ self._advance() # consume IN
1155
+ right = self._parse_bitwise_or()
1156
+ comparators.append(("not in", right))
1157
+ elif self._match_concept("IS"):
1158
+ self._advance()
1159
+ if self._match_concept("NOT"):
1160
+ self._advance()
1161
+ right = self._parse_bitwise_or()
1162
+ comparators.append(("is not", right))
1163
+ else:
1164
+ right = self._parse_bitwise_or()
1165
+ comparators.append(("is", right))
1166
+ else:
1167
+ break
1168
+ if not comparators:
1169
+ return left
1170
+ return CompareOp(left, comparators, line=left.line, column=left.column)
1171
+
1172
+ def _parse_bitwise_or(self):
1173
+ """Parse: bitwise_xor (| bitwise_xor)*."""
1174
+ left = self._parse_bitwise_xor()
1175
+ while self._match_operator("|"):
1176
+ tok = self._advance()
1177
+ right = self._parse_bitwise_xor()
1178
+ left = BinaryOp(left, "|", right,
1179
+ line=tok.line, column=tok.column)
1180
+ return left
1181
+
1182
+ def _parse_bitwise_xor(self):
1183
+ """Parse: bitwise_and (^ bitwise_and)*."""
1184
+ left = self._parse_bitwise_and()
1185
+ while self._match_operator("^"):
1186
+ tok = self._advance()
1187
+ right = self._parse_bitwise_and()
1188
+ left = BinaryOp(left, "^", right,
1189
+ line=tok.line, column=tok.column)
1190
+ return left
1191
+
1192
+ def _parse_bitwise_and(self):
1193
+ """Parse: shift_expr (& shift_expr)*."""
1194
+ left = self._parse_shift_expression()
1195
+ while self._match_operator("&"):
1196
+ tok = self._advance()
1197
+ right = self._parse_shift_expression()
1198
+ left = BinaryOp(left, "&", right,
1199
+ line=tok.line, column=tok.column)
1200
+ return left
1201
+
1202
+ def _parse_shift_expression(self):
1203
+ """Parse: additive (<< additive | >> additive)*."""
1204
+ left = self._parse_additive()
1205
+ while self._current().type == TokenType.OPERATOR \
1206
+ and self._current().value in ("<<", ">>"):
1207
+ tok = self._advance()
1208
+ right = self._parse_additive()
1209
+ left = BinaryOp(left, tok.value, right,
1210
+ line=tok.line, column=tok.column)
1211
+ return left
1212
+
1213
+ def _parse_additive(self):
1214
+ """Parse: multiplicative ((+ | -) multiplicative)*."""
1215
+ left = self._parse_multiplicative()
1216
+ while self._current().type == TokenType.OPERATOR \
1217
+ and self._current().value in ("+", "-"):
1218
+ tok = self._advance()
1219
+ right = self._parse_multiplicative()
1220
+ left = BinaryOp(left, tok.value, right,
1221
+ line=tok.line, column=tok.column)
1222
+ return left
1223
+
1224
+ def _parse_multiplicative(self):
1225
+ """Parse: unary ((* | / | // | %) unary)*."""
1226
+ left = self._parse_unary()
1227
+ while self._current().type == TokenType.OPERATOR \
1228
+ and self._current().value in ("*", "/", "//", "%"):
1229
+ tok = self._advance()
1230
+ right = self._parse_unary()
1231
+ left = BinaryOp(left, tok.value, right,
1232
+ line=tok.line, column=tok.column)
1233
+ return left
1234
+
1235
+ def _parse_unary(self):
1236
+ """Parse: AWAIT unary | (- | + | ~) unary | power."""
1237
+ if self._match_concept("AWAIT"):
1238
+ tok = self._advance()
1239
+ value = self._parse_unary()
1240
+ return AwaitExpr(value, line=tok.line, column=tok.column)
1241
+ if self._current().type == TokenType.OPERATOR \
1242
+ and self._current().value in ("-", "+", "~"):
1243
+ tok = self._advance()
1244
+ operand = self._parse_unary()
1245
+ return UnaryOp(tok.value, operand,
1246
+ line=tok.line, column=tok.column)
1247
+ return self._parse_power()
1248
+
1249
+ def _parse_power(self):
1250
+ """Parse: primary (** unary)? (right-associative)."""
1251
+ base = self._parse_primary()
1252
+ if self._match_operator("**"):
1253
+ tok = self._advance()
1254
+ exponent = self._parse_unary()
1255
+ return BinaryOp(base, "**", exponent,
1256
+ line=tok.line, column=tok.column)
1257
+ return base
1258
+
1259
+ def _parse_primary(self):
1260
+ """Parse: atom trailer* where trailer is (args) or [index] or .attr."""
1261
+ node = self._parse_atom()
1262
+
1263
+ while True:
1264
+ if self._match_delimiter("("):
1265
+ node = self._parse_call(node)
1266
+ elif self._match_delimiter("["):
1267
+ tok = self._advance()
1268
+ index = self._parse_slice_or_index()
1269
+ self._expect_delimiter("]")
1270
+ node = IndexAccess(node, index,
1271
+ line=tok.line, column=tok.column)
1272
+ elif self._match_delimiter("."):
1273
+ tok = self._advance()
1274
+ attr_tok = self._expect_identifier()
1275
+ node = AttributeAccess(node, attr_tok.value,
1276
+ line=tok.line, column=tok.column)
1277
+ else:
1278
+ break
1279
+
1280
+ return node
1281
+
1282
+ def _parse_slice_or_index(self):
1283
+ """Parse index or slice expression inside [].
1284
+
1285
+ Returns a SliceExpr if colons are present, otherwise a normal expression.
1286
+ Handles: [i], [s:e], [s:e:step], [:e], [s:], [::step], [:], [::]
1287
+ """
1288
+ tok = self._current()
1289
+ start = None
1290
+ # Check if we start with a colon (no start expression)
1291
+ if not self._match_delimiter(":"):
1292
+ start = self._parse_expression()
1293
+
1294
+ # If no colon follows, it's a simple index
1295
+ if not self._match_delimiter(":"):
1296
+ return start
1297
+
1298
+ # We have a slice — consume first colon
1299
+ self._advance()
1300
+ stop = None
1301
+ step = None
1302
+
1303
+ # Parse stop (if present and not another colon or ])
1304
+ if not self._match_delimiter("]") and not self._match_delimiter(":"):
1305
+ stop = self._parse_expression()
1306
+
1307
+ # Check for second colon (step)
1308
+ if self._match_delimiter(":"):
1309
+ self._advance()
1310
+ if not self._match_delimiter("]"):
1311
+ step = self._parse_expression()
1312
+
1313
+ return SliceExpr(start, stop, step,
1314
+ line=tok.line, column=tok.column)
1315
+
1316
+ def _parse_call(self, func):
1317
+ """Parse function call arguments: (expr, expr, name=expr, ...)."""
1318
+ tok = self._advance() # consume (
1319
+ args = []
1320
+ keywords = []
1321
+ seen_keyword = False
1322
+ self._skip_newlines()
1323
+
1324
+ if not self._match_delimiter(")"):
1325
+ first_kind = self._parse_argument(args, keywords)
1326
+ if first_kind == "keyword":
1327
+ seen_keyword = True
1328
+ elif first_kind == "positional" and seen_keyword:
1329
+ self._error("UNEXPECTED_TOKEN", self._current(),
1330
+ token="positional argument after keyword argument")
1331
+ # Check for generator expression: func(expr FOR ...)
1332
+ if first_kind == "positional" and len(args) == 1 and not keywords \
1333
+ and self._match_concept("LOOP_FOR"):
1334
+ gen = self._parse_comprehension_tail(
1335
+ args[0], tok, "generator"
1336
+ )
1337
+ # _parse_comprehension_tail consumed the closing )
1338
+ return CallExpr(func, [gen], [],
1339
+ line=tok.line, column=tok.column)
1340
+ while self._match_delimiter(","):
1341
+ self._advance()
1342
+ self._skip_newlines()
1343
+ if self._match_delimiter(")"):
1344
+ break
1345
+ arg_kind = self._parse_argument(args, keywords)
1346
+ if arg_kind == "keyword":
1347
+ seen_keyword = True
1348
+ elif arg_kind == "positional" and seen_keyword:
1349
+ self._error("UNEXPECTED_TOKEN", self._current(),
1350
+ token="positional argument after keyword argument")
1351
+ self._skip_newlines()
1352
+
1353
+ self._skip_newlines()
1354
+ self._expect_delimiter(")")
1355
+ return CallExpr(func, args, keywords,
1356
+ line=tok.line, column=tok.column)
1357
+
1358
+ def _parse_argument(self, args, keywords):
1359
+ """Parse a single argument (positional, keyword, *args, or **kwargs)."""
1360
+ # Check for **kwargs
1361
+ if self._match_operator("**"):
1362
+ tok = self._advance()
1363
+ value = self._parse_expression()
1364
+ args.append(StarredExpr(value, is_double=True,
1365
+ line=tok.line, column=tok.column))
1366
+ return "star"
1367
+
1368
+ # Check for *args
1369
+ if self._match_operator("*"):
1370
+ tok = self._advance()
1371
+ value = self._parse_expression()
1372
+ args.append(StarredExpr(value, is_double=False,
1373
+ line=tok.line, column=tok.column))
1374
+ return "star"
1375
+
1376
+ # Check for keyword argument: name=value
1377
+ if self._match_type(TokenType.IDENTIFIER):
1378
+ # Look ahead for '='
1379
+ save_pos = self.pos
1380
+ name_tok = self._advance()
1381
+ if self._match_operator("="):
1382
+ self._advance()
1383
+ value = self._parse_expression()
1384
+ keywords.append((name_tok.value, value))
1385
+ return "keyword"
1386
+ # Not a keyword arg, restore and parse as expression
1387
+ self.pos = save_pos
1388
+
1389
+ args.append(self._parse_expression())
1390
+ return "positional"
1391
+
1392
+ def _parse_atom(self): # pylint: disable=too-many-branches
1393
+ """Parse atomic expressions: literals, identifiers, parenthesized."""
1394
+ tok = self._current()
1395
+
1396
+ # Numeral literal
1397
+ if tok.type == TokenType.NUMERAL:
1398
+ self._advance()
1399
+ return NumeralLiteral(tok.value,
1400
+ line=tok.line, column=tok.column)
1401
+
1402
+ # String literal
1403
+ if tok.type == TokenType.STRING:
1404
+ self._advance()
1405
+ return StringLiteral(tok.value,
1406
+ line=tok.line, column=tok.column)
1407
+
1408
+ # F-string literal
1409
+ if tok.type == TokenType.FSTRING:
1410
+ self._advance()
1411
+ return self._parse_fstring(tok)
1412
+
1413
+ # Date literal
1414
+ if tok.type == TokenType.DATE_LITERAL:
1415
+ self._advance()
1416
+ return DateLiteral(tok.value,
1417
+ line=tok.line, column=tok.column)
1418
+
1419
+ # Identifier
1420
+ if tok.type == TokenType.IDENTIFIER:
1421
+ self._advance()
1422
+ return Identifier(tok.value,
1423
+ line=tok.line, column=tok.column)
1424
+
1425
+ # Keyword-based atoms
1426
+ if tok.type == TokenType.KEYWORD:
1427
+ concept = tok.concept
1428
+
1429
+ # Boolean literals
1430
+ if concept == "TRUE":
1431
+ self._advance()
1432
+ return BooleanLiteral(True,
1433
+ line=tok.line, column=tok.column)
1434
+ if concept == "FALSE":
1435
+ self._advance()
1436
+ return BooleanLiteral(False,
1437
+ line=tok.line, column=tok.column)
1438
+
1439
+ # None literal
1440
+ if concept == "NONE":
1441
+ self._advance()
1442
+ return NoneLiteral(line=tok.line, column=tok.column)
1443
+
1444
+ # PRINT, INPUT as callable identifiers
1445
+ if concept in _CALLABLE_CONCEPTS:
1446
+ self._advance()
1447
+ return Identifier(tok.value,
1448
+ line=tok.line, column=tok.column)
1449
+
1450
+ # Type keywords as identifiers
1451
+ if concept in _TYPE_CONCEPTS:
1452
+ self._advance()
1453
+ return Identifier(tok.value,
1454
+ line=tok.line, column=tok.column)
1455
+
1456
+ # Lambda
1457
+ if concept == "LAMBDA":
1458
+ return self._parse_lambda()
1459
+
1460
+ # Yield expression
1461
+ if concept == "YIELD":
1462
+ return self._parse_yield_expr()
1463
+
1464
+ # Parenthesized expression or generator expression
1465
+ if self._match_delimiter("("):
1466
+ open_tok = self._advance()
1467
+ if self._match_delimiter(")"):
1468
+ # Empty tuple ()
1469
+ return TupleLiteral([], line=open_tok.line,
1470
+ column=open_tok.column)
1471
+ expr = self._parse_expression()
1472
+ # Check for generator expression: (expr FOR ...)
1473
+ if self._match_concept("LOOP_FOR"):
1474
+ result = self._parse_comprehension_tail(
1475
+ expr, open_tok, "generator"
1476
+ )
1477
+ return result
1478
+ # Tuple literal: (a, b, c)
1479
+ if self._match_delimiter(","):
1480
+ elements = [expr]
1481
+ while self._match_delimiter(","):
1482
+ self._advance()
1483
+ if self._match_delimiter(")"):
1484
+ break
1485
+ elements.append(self._parse_expression())
1486
+ self._expect_delimiter(")")
1487
+ return TupleLiteral(elements, line=open_tok.line,
1488
+ column=open_tok.column)
1489
+ self._expect_delimiter(")")
1490
+ return expr
1491
+
1492
+ # List literal
1493
+ if self._match_delimiter("["):
1494
+ return self._parse_list_literal()
1495
+
1496
+ # Dict / set literal
1497
+ if self._match_delimiter("{"):
1498
+ return self._parse_brace_literal()
1499
+
1500
+ self._error("EXPECTED_EXPRESSION", tok, token=tok.value)
1501
+
1502
+ def _parse_fstring(self, tok): # pylint: disable=too-many-statements
1503
+ """Parse an f-string by extracting {expr} segments from the raw text.
1504
+
1505
+ Handles format specs ({expr:fmt}) and conversions ({expr!r}).
1506
+ """
1507
+ raw = tok.value
1508
+ parts = []
1509
+ i = 0
1510
+ current_text = ""
1511
+ while i < len(raw):
1512
+ ch = raw[i]
1513
+ if ch == "{":
1514
+ if i + 1 < len(raw) and raw[i + 1] == "{":
1515
+ # Escaped {{ → literal {
1516
+ current_text += "{"
1517
+ i += 2
1518
+ continue
1519
+ # Start of expression — save current text
1520
+ if current_text:
1521
+ parts.append(current_text)
1522
+ current_text = ""
1523
+ # Find matching closing }
1524
+ depth = 1
1525
+ i += 1
1526
+ expr_text = ""
1527
+ format_spec = ""
1528
+ conversion = ""
1529
+ in_format = False
1530
+ while i < len(raw) and depth > 0:
1531
+ if raw[i] == "{":
1532
+ depth += 1
1533
+ elif raw[i] == "}":
1534
+ depth -= 1
1535
+ if depth == 0:
1536
+ break
1537
+ # Detect conversion (!r, !s, !a) at depth 1
1538
+ if depth == 1 and not in_format and raw[i] == "!" \
1539
+ and i + 1 < len(raw) and raw[i + 1] in "rsa":
1540
+ conversion = raw[i + 1]
1541
+ i += 2
1542
+ continue
1543
+ # Detect format spec (:...) at depth 1
1544
+ if depth == 1 and not in_format and raw[i] == ":":
1545
+ in_format = True
1546
+ i += 1
1547
+ continue
1548
+ if in_format:
1549
+ format_spec += raw[i]
1550
+ else:
1551
+ expr_text += raw[i]
1552
+ i += 1
1553
+ i += 1 # skip closing }
1554
+ # Parse the expression text
1555
+ sub_lexer = Lexer(expr_text, language=self.source_language)
1556
+ sub_tokens = sub_lexer.tokenize()
1557
+ sub_parser = Parser(sub_tokens, self.source_language)
1558
+ expr_node = sub_parser.parse_expression_fragment()
1559
+ # Attach format spec and conversion as metadata
1560
+ if format_spec or conversion:
1561
+ setattr(expr_node, "fstring_format_spec", format_spec)
1562
+ setattr(expr_node, "fstring_conversion", conversion)
1563
+ parts.append(expr_node)
1564
+ elif ch == "}" and i + 1 < len(raw) and raw[i + 1] == "}":
1565
+ # Escaped }} → literal }
1566
+ current_text += "}"
1567
+ i += 2
1568
+ else:
1569
+ current_text += ch
1570
+ i += 1
1571
+ if current_text:
1572
+ parts.append(current_text)
1573
+ return FStringLiteral(parts, line=tok.line, column=tok.column)
1574
+
1575
+ def _parse_lambda(self):
1576
+ """Parse: LAMBDA params : expression."""
1577
+ tok = self._advance() # consume LAMBDA
1578
+ params = []
1579
+ if not self._match_delimiter(":"):
1580
+ param = self._expect_identifier()
1581
+ params.append(param.value)
1582
+ while self._match_delimiter(","):
1583
+ self._advance()
1584
+ param = self._expect_identifier()
1585
+ params.append(param.value)
1586
+
1587
+ self._expect_delimiter(":")
1588
+ body = self._parse_expression()
1589
+ return LambdaExpr(params, body, line=tok.line, column=tok.column)
1590
+
1591
+ def _parse_yield_expr(self):
1592
+ """Parse: YIELD [FROM] [expression]."""
1593
+ tok = self._advance() # consume YIELD
1594
+ is_from = False
1595
+ if self._match_concept("FROM"):
1596
+ self._advance()
1597
+ is_from = True
1598
+ value = None
1599
+ if not self._at_end() and not self._match_type(TokenType.NEWLINE) \
1600
+ and not self._match_type(TokenType.DEDENT) \
1601
+ and not self._match_delimiter(")") \
1602
+ and not self._match_delimiter(","):
1603
+ value = self._parse_expression()
1604
+ return YieldExpr(value, is_from=is_from,
1605
+ line=tok.line, column=tok.column)
1606
+
1607
+ def _parse_list_literal(self):
1608
+ """Parse: [ expr, expr, ... ] or [expr for target in iter [if cond]]."""
1609
+ tok = self._advance() # consume [
1610
+ if self._match_delimiter("]"):
1611
+ self._advance() # consume ]
1612
+ return ListLiteral([], line=tok.line, column=tok.column)
1613
+
1614
+ first = self._parse_expression()
1615
+
1616
+ # Check for list comprehension: [expr FOR ...]
1617
+ if self._match_concept("LOOP_FOR"):
1618
+ return self._parse_comprehension_tail(
1619
+ first, tok, "list"
1620
+ )
1621
+
1622
+ elements = [first]
1623
+ while self._match_delimiter(","):
1624
+ self._advance()
1625
+ if self._match_delimiter("]"):
1626
+ break
1627
+ elements.append(self._parse_expression())
1628
+ self._expect_delimiter("]")
1629
+ return ListLiteral(elements, line=tok.line, column=tok.column)
1630
+
1631
+ def _parse_brace_literal(self):
1632
+ """Parse dict or set literal, including dict unpacking."""
1633
+ tok = self._advance() # consume {
1634
+ if self._match_delimiter("}"):
1635
+ self._advance() # consume }
1636
+ return DictLiteral([], line=tok.line, column=tok.column)
1637
+
1638
+ # Dict unpack at start
1639
+ if self._match_operator("**"):
1640
+ entries = [self._parse_dict_unpack_entry()]
1641
+ while self._match_delimiter(","):
1642
+ self._advance()
1643
+ if self._match_delimiter("}"):
1644
+ break
1645
+ if self._match_operator("**"):
1646
+ entries.append(self._parse_dict_unpack_entry())
1647
+ else:
1648
+ key = self._parse_expression()
1649
+ self._expect_delimiter(":")
1650
+ value = self._parse_expression()
1651
+ entries.append((key, value))
1652
+ self._expect_delimiter("}")
1653
+ return DictLiteral(entries, line=tok.line, column=tok.column)
1654
+
1655
+ first = self._parse_expression()
1656
+
1657
+ # Dict literal/comprehension
1658
+ if self._match_delimiter(":"):
1659
+ self._advance()
1660
+ value = self._parse_expression()
1661
+
1662
+ # Check for dict comprehension: {k: v FOR ...}
1663
+ if self._match_concept("LOOP_FOR"):
1664
+ return self._parse_dict_comprehension_tail(
1665
+ first, value, tok
1666
+ )
1667
+
1668
+ entries = [(first, value)]
1669
+ while self._match_delimiter(","):
1670
+ self._advance()
1671
+ if self._match_delimiter("}"):
1672
+ break
1673
+ if self._match_operator("**"):
1674
+ entries.append(self._parse_dict_unpack_entry())
1675
+ continue
1676
+ key = self._parse_expression()
1677
+ self._expect_delimiter(":")
1678
+ value = self._parse_expression()
1679
+ entries.append((key, value))
1680
+ self._expect_delimiter("}")
1681
+ return DictLiteral(entries, line=tok.line, column=tok.column)
1682
+
1683
+ # Set comprehension: {expr FOR ...}
1684
+ if self._match_concept("LOOP_FOR"):
1685
+ return self._parse_set_comprehension_tail(first, tok)
1686
+
1687
+ # Set literal
1688
+ elements = [first]
1689
+ while self._match_delimiter(","):
1690
+ self._advance()
1691
+ if self._match_delimiter("}"):
1692
+ break
1693
+ elements.append(self._parse_expression())
1694
+ self._expect_delimiter("}")
1695
+ return SetLiteral(elements, line=tok.line, column=tok.column)
1696
+
1697
+ def _parse_dict_unpack_entry(self):
1698
+ """Parse a dict unpack element: **expr."""
1699
+ tok = self._advance() # consume **
1700
+ value = self._parse_expression()
1701
+ return DictUnpackEntry(value, line=tok.line, column=tok.column)
1702
+
1703
+ def _parse_comp_target(self):
1704
+ """Parse a comprehension target: single identifier or tuple (a, b)."""
1705
+ first_tok = self._expect_identifier()
1706
+ target = Identifier(first_tok.value,
1707
+ line=first_tok.line, column=first_tok.column)
1708
+ if self._match_delimiter(","):
1709
+ elements = [target]
1710
+ while self._match_delimiter(","):
1711
+ self._advance()
1712
+ next_tok = self._expect_identifier()
1713
+ elements.append(Identifier(
1714
+ next_tok.value,
1715
+ line=next_tok.line, column=next_tok.column
1716
+ ))
1717
+ target = TupleLiteral(elements,
1718
+ line=first_tok.line, column=first_tok.column)
1719
+ return target
1720
+
1721
+ def _parse_comprehension_tail(self, element, tok, kind):
1722
+ """Parse: FOR target IN iterable [IF cond]... and close bracket.
1723
+
1724
+ `element` is the already-parsed element expression.
1725
+ `kind` is 'list' or 'generator'.
1726
+ Uses _parse_or_expression for iterable/conditions to avoid
1727
+ consuming the comprehension 'if' as a ternary operator.
1728
+ """
1729
+ clauses = self._parse_comprehension_clauses()
1730
+ first = clauses[0]
1731
+
1732
+ if kind == "list":
1733
+ self._expect_delimiter("]")
1734
+ return ListComprehension(
1735
+ element, first.target, first.iterable, first.conditions,
1736
+ clauses=clauses,
1737
+ line=tok.line, column=tok.column
1738
+ )
1739
+ # generator
1740
+ self._expect_delimiter(")")
1741
+ return GeneratorExpr(
1742
+ element, first.target, first.iterable, first.conditions,
1743
+ clauses=clauses,
1744
+ line=tok.line, column=tok.column
1745
+ )
1746
+
1747
+ def _parse_dict_comprehension_tail(self, key, value, tok):
1748
+ """Parse: FOR target IN iterable [IF cond]... }."""
1749
+ clauses = self._parse_comprehension_clauses()
1750
+ first = clauses[0]
1751
+
1752
+ self._expect_delimiter("}")
1753
+ return DictComprehension(
1754
+ key, value, first.target, first.iterable, first.conditions,
1755
+ clauses=clauses,
1756
+ line=tok.line, column=tok.column
1757
+ )
1758
+
1759
+ def _parse_set_comprehension_tail(self, element, tok):
1760
+ """Parse: FOR target IN iterable [IF cond]... }."""
1761
+ clauses = self._parse_comprehension_clauses()
1762
+ first = clauses[0]
1763
+
1764
+ self._expect_delimiter("}")
1765
+ return SetComprehension(
1766
+ element, first.target, first.iterable, first.conditions,
1767
+ clauses=clauses,
1768
+ line=tok.line, column=tok.column
1769
+ )
1770
+
1771
+ def _parse_comprehension_clauses(self):
1772
+ """Parse one or more comprehension clauses.
1773
+
1774
+ Grammar: (FOR target IN iterable [IF cond]...)+
1775
+ """
1776
+ clauses = []
1777
+ while self._match_concept("LOOP_FOR"):
1778
+ for_tok = self._advance() # consume FOR keyword
1779
+ target = self._parse_comp_target()
1780
+ self._expect_concept("IN")
1781
+ iterable = self._parse_or_expression()
1782
+
1783
+ conditions = []
1784
+ while self._match_concept("COND_IF"):
1785
+ self._advance()
1786
+ conditions.append(self._parse_or_expression())
1787
+
1788
+ clauses.append(ComprehensionClause(
1789
+ target, iterable, conditions,
1790
+ line=for_tok.line, column=for_tok.column
1791
+ ))
1792
+
1793
+ if not clauses:
1794
+ self._error("UNEXPECTED_TOKEN", self._current(),
1795
+ token=self._current().value)
1796
+ return clauses