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.
- multilingualprogramming/__init__.py +74 -0
- multilingualprogramming/__main__.py +194 -0
- multilingualprogramming/codegen/__init__.py +12 -0
- multilingualprogramming/codegen/executor.py +215 -0
- multilingualprogramming/codegen/python_generator.py +592 -0
- multilingualprogramming/codegen/repl.py +489 -0
- multilingualprogramming/codegen/runtime_builtins.py +308 -0
- multilingualprogramming/core/__init__.py +12 -0
- multilingualprogramming/core/ir.py +29 -0
- multilingualprogramming/core/lowering.py +24 -0
- multilingualprogramming/datetime/__init__.py +11 -0
- multilingualprogramming/datetime/date_parser.py +190 -0
- multilingualprogramming/datetime/mp_date.py +210 -0
- multilingualprogramming/datetime/mp_datetime.py +153 -0
- multilingualprogramming/datetime/mp_time.py +147 -0
- multilingualprogramming/datetime/resource_loader.py +18 -0
- multilingualprogramming/exceptions.py +158 -0
- multilingualprogramming/imports.py +150 -0
- multilingualprogramming/keyword/__init__.py +13 -0
- multilingualprogramming/keyword/keyword_registry.py +249 -0
- multilingualprogramming/keyword/keyword_validator.py +59 -0
- multilingualprogramming/keyword/language_pack_validator.py +110 -0
- multilingualprogramming/lexer/__init__.py +11 -0
- multilingualprogramming/lexer/lexer.py +570 -0
- multilingualprogramming/lexer/source_reader.py +91 -0
- multilingualprogramming/lexer/token.py +54 -0
- multilingualprogramming/lexer/token_types.py +38 -0
- multilingualprogramming/numeral/__init__.py +11 -0
- multilingualprogramming/numeral/abstract_numeral.py +232 -0
- multilingualprogramming/numeral/complex_numeral.py +190 -0
- multilingualprogramming/numeral/fraction_numeral.py +165 -0
- multilingualprogramming/numeral/mp_numeral.py +243 -0
- multilingualprogramming/numeral/numeral_converter.py +151 -0
- multilingualprogramming/numeral/roman_numeral.py +301 -0
- multilingualprogramming/numeral/unicode_numeral.py +292 -0
- multilingualprogramming/parser/__init__.py +28 -0
- multilingualprogramming/parser/ast_nodes.py +459 -0
- multilingualprogramming/parser/ast_printer.py +677 -0
- multilingualprogramming/parser/error_messages.py +75 -0
- multilingualprogramming/parser/parser.py +1796 -0
- multilingualprogramming/parser/semantic_analyzer.py +689 -0
- multilingualprogramming/parser/surface_normalizer.py +282 -0
- multilingualprogramming/resources/datetime/eras.json +23 -0
- multilingualprogramming/resources/datetime/formats.json +32 -0
- multilingualprogramming/resources/datetime/months.json +150 -0
- multilingualprogramming/resources/datetime/weekdays.json +90 -0
- multilingualprogramming/resources/parser/error_messages.json +310 -0
- multilingualprogramming/resources/repl/commands.json +636 -0
- multilingualprogramming/resources/usm/builtins_aliases.json +731 -0
- multilingualprogramming/resources/usm/keywords.json +1063 -0
- multilingualprogramming/resources/usm/operators.json +532 -0
- multilingualprogramming/resources/usm/schema.json +34 -0
- multilingualprogramming/resources/usm/surface_patterns.json +1523 -0
- multilingualprogramming/unicode_string.py +140 -0
- multilingualprogramming/version.py +9 -0
- multilingualprogramming-0.2.0.dist-info/METADATA +350 -0
- multilingualprogramming-0.2.0.dist-info/RECORD +61 -0
- multilingualprogramming-0.2.0.dist-info/WHEEL +5 -0
- multilingualprogramming-0.2.0.dist-info/entry_points.txt +3 -0
- multilingualprogramming-0.2.0.dist-info/licenses/LICENSE +674 -0
- 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
|