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,592 @@
|
|
|
1
|
+
#
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Python code generator: transpiles the multilingual AST to valid Python source."""
|
|
8
|
+
|
|
9
|
+
from multilingualprogramming.exceptions import CodeGenerationError
|
|
10
|
+
from multilingualprogramming.numeral.mp_numeral import MPNumeral
|
|
11
|
+
from multilingualprogramming.core.ir import CoreIRProgram
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PythonCodeGenerator:
|
|
15
|
+
"""
|
|
16
|
+
Visitor-based transpiler that converts a multilingual AST into
|
|
17
|
+
valid Python 3 source code.
|
|
18
|
+
|
|
19
|
+
NumeralLiteral values in any Unicode script are converted to Python
|
|
20
|
+
numeric literals via MPNumeral.to_decimal().
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
gen = PythonCodeGenerator()
|
|
24
|
+
python_source = gen.generate(ast_program)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, indent_str=" "):
|
|
28
|
+
self.indent_str = indent_str
|
|
29
|
+
self._depth = 0
|
|
30
|
+
self._lines = []
|
|
31
|
+
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
# Public API
|
|
34
|
+
# ------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
def generate(self, node):
|
|
37
|
+
"""Generate Python source from the AST root node."""
|
|
38
|
+
if isinstance(node, CoreIRProgram):
|
|
39
|
+
node = node.ast
|
|
40
|
+
self._depth = 0
|
|
41
|
+
self._lines = []
|
|
42
|
+
node.accept(self)
|
|
43
|
+
return "\n".join(self._lines) + "\n"
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# Helpers
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def _emit(self, text):
|
|
50
|
+
"""Add a line at the current indentation level."""
|
|
51
|
+
self._lines.append(self.indent_str * self._depth + text)
|
|
52
|
+
|
|
53
|
+
def _indent(self):
|
|
54
|
+
self._depth += 1
|
|
55
|
+
|
|
56
|
+
def _dedent(self):
|
|
57
|
+
self._depth -= 1
|
|
58
|
+
|
|
59
|
+
def _emit_body(self, body):
|
|
60
|
+
"""Emit a list of statements as an indented block."""
|
|
61
|
+
self._indent()
|
|
62
|
+
if not body:
|
|
63
|
+
self._emit("pass")
|
|
64
|
+
else:
|
|
65
|
+
for stmt in body:
|
|
66
|
+
stmt.accept(self)
|
|
67
|
+
self._dedent()
|
|
68
|
+
|
|
69
|
+
def _expr(self, node):
|
|
70
|
+
"""Generate the expression string for a node.
|
|
71
|
+
|
|
72
|
+
Uses a sub-generator so expression visitors can return strings
|
|
73
|
+
rather than emitting lines.
|
|
74
|
+
"""
|
|
75
|
+
sub = _ExpressionGenerator()
|
|
76
|
+
return node.accept(sub)
|
|
77
|
+
|
|
78
|
+
def _error(self, message, node):
|
|
79
|
+
"""Raise a CodeGenerationError with source location."""
|
|
80
|
+
raise CodeGenerationError(message, node.line, node.column)
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Statement visitors (emit lines)
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def visit_Program(self, node):
|
|
87
|
+
for stmt in node.body:
|
|
88
|
+
stmt.accept(self)
|
|
89
|
+
|
|
90
|
+
def visit_VariableDeclaration(self, node):
|
|
91
|
+
val = self._expr(node.value)
|
|
92
|
+
self._emit(f"{node.name} = {val}")
|
|
93
|
+
|
|
94
|
+
def visit_Assignment(self, node):
|
|
95
|
+
target = self._expr(node.target)
|
|
96
|
+
val = self._expr(node.value)
|
|
97
|
+
self._emit(f"{target} {node.op} {val}")
|
|
98
|
+
|
|
99
|
+
def visit_AnnAssignment(self, node):
|
|
100
|
+
target = self._expr(node.target)
|
|
101
|
+
annotation = self._expr(node.annotation)
|
|
102
|
+
if node.value is None:
|
|
103
|
+
self._emit(f"{target}: {annotation}")
|
|
104
|
+
else:
|
|
105
|
+
val = self._expr(node.value)
|
|
106
|
+
self._emit(f"{target}: {annotation} = {val}")
|
|
107
|
+
|
|
108
|
+
def visit_ExpressionStatement(self, node):
|
|
109
|
+
expr = self._expr(node.expression)
|
|
110
|
+
self._emit(expr)
|
|
111
|
+
|
|
112
|
+
def visit_PassStatement(self, _node):
|
|
113
|
+
self._emit("pass")
|
|
114
|
+
|
|
115
|
+
def visit_ReturnStatement(self, node):
|
|
116
|
+
if node.value:
|
|
117
|
+
val = self._expr(node.value)
|
|
118
|
+
self._emit(f"return {val}")
|
|
119
|
+
else:
|
|
120
|
+
self._emit("return")
|
|
121
|
+
|
|
122
|
+
def visit_BreakStatement(self, _node):
|
|
123
|
+
self._emit("break")
|
|
124
|
+
|
|
125
|
+
def visit_ContinueStatement(self, _node):
|
|
126
|
+
self._emit("continue")
|
|
127
|
+
|
|
128
|
+
def visit_RaiseStatement(self, node):
|
|
129
|
+
if node.value:
|
|
130
|
+
val = self._expr(node.value)
|
|
131
|
+
if getattr(node, "cause", None):
|
|
132
|
+
cause = self._expr(node.cause)
|
|
133
|
+
self._emit(f"raise {val} from {cause}")
|
|
134
|
+
else:
|
|
135
|
+
self._emit(f"raise {val}")
|
|
136
|
+
else:
|
|
137
|
+
self._emit("raise")
|
|
138
|
+
|
|
139
|
+
def visit_DelStatement(self, node):
|
|
140
|
+
target = self._expr(node.target)
|
|
141
|
+
self._emit(f"del {target}")
|
|
142
|
+
|
|
143
|
+
def visit_AssertStatement(self, node):
|
|
144
|
+
test = self._expr(node.test)
|
|
145
|
+
if node.msg:
|
|
146
|
+
msg = self._expr(node.msg)
|
|
147
|
+
self._emit(f"assert {test}, {msg}")
|
|
148
|
+
else:
|
|
149
|
+
self._emit(f"assert {test}")
|
|
150
|
+
|
|
151
|
+
def visit_ChainedAssignment(self, node):
|
|
152
|
+
targets = " = ".join(self._expr(t) for t in node.targets)
|
|
153
|
+
value = self._expr(node.value)
|
|
154
|
+
self._emit(f"{targets} = {value}")
|
|
155
|
+
|
|
156
|
+
def visit_GlobalStatement(self, node):
|
|
157
|
+
names = ", ".join(node.names)
|
|
158
|
+
self._emit(f"global {names}")
|
|
159
|
+
|
|
160
|
+
def visit_LocalStatement(self, node):
|
|
161
|
+
names = ", ".join(node.names)
|
|
162
|
+
self._emit(f"nonlocal {names}")
|
|
163
|
+
|
|
164
|
+
def visit_YieldStatement(self, node):
|
|
165
|
+
keyword = "yield from" if getattr(node, "is_from", False) else "yield"
|
|
166
|
+
if node.value:
|
|
167
|
+
val = self._expr(node.value)
|
|
168
|
+
self._emit(f"{keyword} {val}")
|
|
169
|
+
else:
|
|
170
|
+
self._emit(keyword)
|
|
171
|
+
|
|
172
|
+
# -- Compound statements --
|
|
173
|
+
|
|
174
|
+
def visit_IfStatement(self, node):
|
|
175
|
+
cond = self._expr(node.condition)
|
|
176
|
+
self._emit(f"if {cond}:")
|
|
177
|
+
self._emit_body(node.body)
|
|
178
|
+
for elif_cond, elif_body in node.elif_clauses:
|
|
179
|
+
econd = self._expr(elif_cond)
|
|
180
|
+
self._emit(f"elif {econd}:")
|
|
181
|
+
self._emit_body(elif_body)
|
|
182
|
+
if node.else_body:
|
|
183
|
+
self._emit("else:")
|
|
184
|
+
self._emit_body(node.else_body)
|
|
185
|
+
|
|
186
|
+
def visit_WhileLoop(self, node):
|
|
187
|
+
cond = self._expr(node.condition)
|
|
188
|
+
self._emit(f"while {cond}:")
|
|
189
|
+
self._emit_body(node.body)
|
|
190
|
+
if node.else_body:
|
|
191
|
+
self._emit("else:")
|
|
192
|
+
self._emit_body(node.else_body)
|
|
193
|
+
|
|
194
|
+
def visit_ForLoop(self, node):
|
|
195
|
+
target = self._expr(node.target)
|
|
196
|
+
iterable = self._expr(node.iterable)
|
|
197
|
+
prefix = "async " if getattr(node, "is_async", False) else ""
|
|
198
|
+
self._emit(f"{prefix}for {target} in {iterable}:")
|
|
199
|
+
self._emit_body(node.body)
|
|
200
|
+
if getattr(node, "else_body", None):
|
|
201
|
+
self._emit("else:")
|
|
202
|
+
self._emit_body(node.else_body)
|
|
203
|
+
|
|
204
|
+
def visit_FunctionDef(self, node):
|
|
205
|
+
# Emit decorators
|
|
206
|
+
for dec in getattr(node, 'decorators', []):
|
|
207
|
+
dec_expr = self._expr(dec)
|
|
208
|
+
self._emit(f"@{dec_expr}")
|
|
209
|
+
# Build parameter list
|
|
210
|
+
param_strs = []
|
|
211
|
+
for param in node.params:
|
|
212
|
+
if isinstance(param, str):
|
|
213
|
+
param_strs.append(param)
|
|
214
|
+
else:
|
|
215
|
+
param_strs.append(self._expr(param))
|
|
216
|
+
params = ", ".join(param_strs)
|
|
217
|
+
prefix = "async " if getattr(node, "is_async", False) else ""
|
|
218
|
+
ret_ann = ""
|
|
219
|
+
if getattr(node, "return_annotation", None) is not None:
|
|
220
|
+
ret_ann = f" -> {self._expr(node.return_annotation)}"
|
|
221
|
+
self._emit(f"{prefix}def {node.name}({params}){ret_ann}:")
|
|
222
|
+
self._emit_body(node.body)
|
|
223
|
+
|
|
224
|
+
def visit_ClassDef(self, node):
|
|
225
|
+
# Emit decorators
|
|
226
|
+
for dec in getattr(node, 'decorators', []):
|
|
227
|
+
dec_expr = self._expr(dec)
|
|
228
|
+
self._emit(f"@{dec_expr}")
|
|
229
|
+
if node.bases:
|
|
230
|
+
bases = ", ".join(self._expr(b) for b in node.bases)
|
|
231
|
+
self._emit(f"class {node.name}({bases}):")
|
|
232
|
+
else:
|
|
233
|
+
self._emit(f"class {node.name}:")
|
|
234
|
+
self._emit_body(node.body)
|
|
235
|
+
|
|
236
|
+
def visit_TryStatement(self, node):
|
|
237
|
+
self._emit("try:")
|
|
238
|
+
self._emit_body(node.body)
|
|
239
|
+
for handler in node.handlers:
|
|
240
|
+
handler.accept(self)
|
|
241
|
+
if node.else_body:
|
|
242
|
+
self._emit("else:")
|
|
243
|
+
self._emit_body(node.else_body)
|
|
244
|
+
if node.finally_body:
|
|
245
|
+
self._emit("finally:")
|
|
246
|
+
self._emit_body(node.finally_body)
|
|
247
|
+
|
|
248
|
+
def visit_ExceptHandler(self, node):
|
|
249
|
+
if node.exc_type:
|
|
250
|
+
exc = self._expr(node.exc_type)
|
|
251
|
+
if node.name:
|
|
252
|
+
self._emit(f"except {exc} as {node.name}:")
|
|
253
|
+
else:
|
|
254
|
+
self._emit(f"except {exc}:")
|
|
255
|
+
else:
|
|
256
|
+
self._emit("except:")
|
|
257
|
+
self._emit_body(node.body)
|
|
258
|
+
|
|
259
|
+
def visit_MatchStatement(self, node):
|
|
260
|
+
subject = self._expr(node.subject)
|
|
261
|
+
self._emit(f"match {subject}:")
|
|
262
|
+
self._indent()
|
|
263
|
+
for case in node.cases:
|
|
264
|
+
case.accept(self)
|
|
265
|
+
self._dedent()
|
|
266
|
+
|
|
267
|
+
def visit_CaseClause(self, node):
|
|
268
|
+
if node.is_default:
|
|
269
|
+
self._emit("case _:")
|
|
270
|
+
else:
|
|
271
|
+
pattern = self._expr(node.pattern)
|
|
272
|
+
guard = ""
|
|
273
|
+
if getattr(node, "guard", None):
|
|
274
|
+
guard = f" if {self._expr(node.guard)}"
|
|
275
|
+
self._emit(f"case {pattern}{guard}:")
|
|
276
|
+
self._emit_body(node.body)
|
|
277
|
+
|
|
278
|
+
def visit_WithStatement(self, node):
|
|
279
|
+
parts = []
|
|
280
|
+
for ctx_expr, name in node.items:
|
|
281
|
+
ctx = self._expr(ctx_expr)
|
|
282
|
+
if name:
|
|
283
|
+
parts.append(f"{ctx} as {name}")
|
|
284
|
+
else:
|
|
285
|
+
parts.append(ctx)
|
|
286
|
+
prefix = "async " if getattr(node, "is_async", False) else ""
|
|
287
|
+
self._emit(f"{prefix}with {', '.join(parts)}:")
|
|
288
|
+
self._emit_body(node.body)
|
|
289
|
+
|
|
290
|
+
def visit_ImportStatement(self, node):
|
|
291
|
+
if node.alias:
|
|
292
|
+
self._emit(f"import {node.module} as {node.alias}")
|
|
293
|
+
else:
|
|
294
|
+
self._emit(f"import {node.module}")
|
|
295
|
+
|
|
296
|
+
def visit_FromImportStatement(self, node):
|
|
297
|
+
parts = []
|
|
298
|
+
for name, alias in node.names:
|
|
299
|
+
if alias:
|
|
300
|
+
parts.append(f"{name} as {alias}")
|
|
301
|
+
else:
|
|
302
|
+
parts.append(name)
|
|
303
|
+
names = ", ".join(parts)
|
|
304
|
+
self._emit(f"from {node.module} import {names}")
|
|
305
|
+
|
|
306
|
+
def generic_visit(self, node):
|
|
307
|
+
"""Raise when statement node code generation is not implemented."""
|
|
308
|
+
self._error(
|
|
309
|
+
f"Unsupported AST node type: {type(node).__name__}", node
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ======================================================================
|
|
314
|
+
# Expression sub-generator (returns strings instead of emitting lines)
|
|
315
|
+
# ======================================================================
|
|
316
|
+
|
|
317
|
+
class _ExpressionGenerator:
|
|
318
|
+
"""Visitor that returns Python expression strings."""
|
|
319
|
+
|
|
320
|
+
def _expr(self, node):
|
|
321
|
+
"""Recursively generate an expression string."""
|
|
322
|
+
return node.accept(self)
|
|
323
|
+
|
|
324
|
+
def _comprehension_clauses(self, node):
|
|
325
|
+
"""Return comprehension clauses with backward compatibility."""
|
|
326
|
+
clauses = getattr(node, "clauses", None)
|
|
327
|
+
if clauses:
|
|
328
|
+
return clauses
|
|
329
|
+
return [node]
|
|
330
|
+
|
|
331
|
+
def _convert_numeral(self, raw_value):
|
|
332
|
+
"""Convert a multilingual numeral string to a Python numeric literal."""
|
|
333
|
+
try:
|
|
334
|
+
num = MPNumeral(raw_value)
|
|
335
|
+
decimal = num.to_decimal()
|
|
336
|
+
# Preserve integer vs float
|
|
337
|
+
if isinstance(decimal, float):
|
|
338
|
+
return repr(decimal)
|
|
339
|
+
return str(decimal)
|
|
340
|
+
except Exception:
|
|
341
|
+
# If MPNumeral can't parse it, try as a raw Python number
|
|
342
|
+
try:
|
|
343
|
+
if isinstance(raw_value, str) and raw_value.lower().startswith(
|
|
344
|
+
("0x", "0o", "0b")
|
|
345
|
+
):
|
|
346
|
+
return str(int(raw_value, 0))
|
|
347
|
+
val = int(raw_value)
|
|
348
|
+
return str(val)
|
|
349
|
+
except ValueError:
|
|
350
|
+
try:
|
|
351
|
+
val = float(raw_value)
|
|
352
|
+
return repr(val)
|
|
353
|
+
except ValueError:
|
|
354
|
+
return raw_value
|
|
355
|
+
|
|
356
|
+
# -- Literals --
|
|
357
|
+
|
|
358
|
+
def visit_NumeralLiteral(self, node):
|
|
359
|
+
return self._convert_numeral(node.value)
|
|
360
|
+
|
|
361
|
+
def visit_StringLiteral(self, node):
|
|
362
|
+
return repr(node.value)
|
|
363
|
+
|
|
364
|
+
def visit_DateLiteral(self, node):
|
|
365
|
+
# Emit as a string for runtime parsing
|
|
366
|
+
return repr(node.value)
|
|
367
|
+
|
|
368
|
+
def visit_BooleanLiteral(self, node):
|
|
369
|
+
return "True" if node.value else "False"
|
|
370
|
+
|
|
371
|
+
def visit_NoneLiteral(self, _node):
|
|
372
|
+
return "None"
|
|
373
|
+
|
|
374
|
+
def visit_ListLiteral(self, node):
|
|
375
|
+
elems = ", ".join(self._expr(e) for e in node.elements)
|
|
376
|
+
return f"[{elems}]"
|
|
377
|
+
|
|
378
|
+
def visit_DictLiteral(self, node):
|
|
379
|
+
parts = []
|
|
380
|
+
for entry in node.entries:
|
|
381
|
+
if isinstance(entry, tuple):
|
|
382
|
+
key, value = entry
|
|
383
|
+
parts.append(f"{self._expr(key)}: {self._expr(value)}")
|
|
384
|
+
else:
|
|
385
|
+
parts.append(self._expr(entry))
|
|
386
|
+
return "{" + ", ".join(parts) + "}"
|
|
387
|
+
|
|
388
|
+
def visit_SetLiteral(self, node):
|
|
389
|
+
elems = ", ".join(self._expr(e) for e in node.elements)
|
|
390
|
+
return "{" + elems + "}"
|
|
391
|
+
|
|
392
|
+
def visit_DictUnpackEntry(self, node):
|
|
393
|
+
return f"**{self._expr(node.value)}"
|
|
394
|
+
|
|
395
|
+
# -- Expressions --
|
|
396
|
+
|
|
397
|
+
def visit_Identifier(self, node):
|
|
398
|
+
return node.name
|
|
399
|
+
|
|
400
|
+
def visit_BinaryOp(self, node):
|
|
401
|
+
left = self._expr(node.left)
|
|
402
|
+
right = self._expr(node.right)
|
|
403
|
+
return f"({left} {node.op} {right})"
|
|
404
|
+
|
|
405
|
+
def visit_UnaryOp(self, node):
|
|
406
|
+
operand = self._expr(node.operand)
|
|
407
|
+
if node.op == "NOT":
|
|
408
|
+
return f"(not {operand})"
|
|
409
|
+
if node.op == "~":
|
|
410
|
+
return f"(~{operand})"
|
|
411
|
+
return f"({node.op}{operand})"
|
|
412
|
+
|
|
413
|
+
def visit_BooleanOp(self, node):
|
|
414
|
+
op_str = " and " if node.op == "AND" else " or "
|
|
415
|
+
parts = [self._expr(v) for v in node.values]
|
|
416
|
+
return "(" + op_str.join(parts) + ")"
|
|
417
|
+
|
|
418
|
+
def visit_CompareOp(self, node):
|
|
419
|
+
parts = [self._expr(node.left)]
|
|
420
|
+
for op, right in node.comparators:
|
|
421
|
+
parts.append(op)
|
|
422
|
+
parts.append(self._expr(right))
|
|
423
|
+
return "(" + " ".join(parts) + ")"
|
|
424
|
+
|
|
425
|
+
def visit_CallExpr(self, node):
|
|
426
|
+
func = self._expr(node.func)
|
|
427
|
+
args = [self._expr(a) for a in node.args]
|
|
428
|
+
kwargs = [f"{name}={self._expr(val)}" for name, val in node.keywords]
|
|
429
|
+
all_args = ", ".join(args + kwargs)
|
|
430
|
+
return f"{func}({all_args})"
|
|
431
|
+
|
|
432
|
+
def visit_AttributeAccess(self, node):
|
|
433
|
+
obj = self._expr(node.obj)
|
|
434
|
+
return f"{obj}.{node.attr}"
|
|
435
|
+
|
|
436
|
+
def visit_IndexAccess(self, node):
|
|
437
|
+
obj = self._expr(node.obj)
|
|
438
|
+
index = self._expr(node.index)
|
|
439
|
+
return f"{obj}[{index}]"
|
|
440
|
+
|
|
441
|
+
def visit_LambdaExpr(self, node):
|
|
442
|
+
param_strs = []
|
|
443
|
+
for p in node.params:
|
|
444
|
+
if isinstance(p, str):
|
|
445
|
+
param_strs.append(p)
|
|
446
|
+
else:
|
|
447
|
+
param_strs.append(self._expr(p))
|
|
448
|
+
params = ", ".join(param_strs)
|
|
449
|
+
body = self._expr(node.body)
|
|
450
|
+
return f"(lambda {params}: {body})"
|
|
451
|
+
|
|
452
|
+
def visit_YieldExpr(self, node):
|
|
453
|
+
keyword = "yield from" if getattr(node, "is_from", False) else "yield"
|
|
454
|
+
if node.value:
|
|
455
|
+
val = self._expr(node.value)
|
|
456
|
+
return f"({keyword} {val})"
|
|
457
|
+
return f"({keyword})"
|
|
458
|
+
|
|
459
|
+
def visit_AwaitExpr(self, node):
|
|
460
|
+
val = self._expr(node.value)
|
|
461
|
+
return f"(await {val})"
|
|
462
|
+
|
|
463
|
+
def visit_NamedExpr(self, node):
|
|
464
|
+
target = self._expr(node.target)
|
|
465
|
+
value = self._expr(node.value)
|
|
466
|
+
return f"({target} := {value})"
|
|
467
|
+
|
|
468
|
+
def visit_ConditionalExpr(self, node):
|
|
469
|
+
true_expr = self._expr(node.true_expr)
|
|
470
|
+
cond = self._expr(node.condition)
|
|
471
|
+
false_expr = self._expr(node.false_expr)
|
|
472
|
+
return f"({true_expr} if {cond} else {false_expr})"
|
|
473
|
+
|
|
474
|
+
def visit_SliceExpr(self, node):
|
|
475
|
+
start = self._expr(node.start) if node.start else ""
|
|
476
|
+
stop = self._expr(node.stop) if node.stop else ""
|
|
477
|
+
if node.step is not None:
|
|
478
|
+
step = self._expr(node.step)
|
|
479
|
+
return f"{start}:{stop}:{step}"
|
|
480
|
+
return f"{start}:{stop}"
|
|
481
|
+
|
|
482
|
+
def visit_Parameter(self, node):
|
|
483
|
+
# Handle separator markers: bare * and /
|
|
484
|
+
if node.name in ("*", "/"):
|
|
485
|
+
return node.name
|
|
486
|
+
prefix = ""
|
|
487
|
+
if node.is_kwarg:
|
|
488
|
+
prefix = "**"
|
|
489
|
+
elif node.is_vararg:
|
|
490
|
+
prefix = "*"
|
|
491
|
+
annotation = ""
|
|
492
|
+
if getattr(node, "annotation", None):
|
|
493
|
+
annotation = f": {self._expr(node.annotation)}"
|
|
494
|
+
if node.default:
|
|
495
|
+
default_expr = self._expr(node.default)
|
|
496
|
+
return f"{prefix}{node.name}{annotation}={default_expr}"
|
|
497
|
+
return f"{prefix}{node.name}{annotation}"
|
|
498
|
+
|
|
499
|
+
def visit_StarredExpr(self, node):
|
|
500
|
+
val = self._expr(node.value)
|
|
501
|
+
prefix = "**" if node.is_double else "*"
|
|
502
|
+
return f"{prefix}{val}"
|
|
503
|
+
|
|
504
|
+
def visit_TupleLiteral(self, node):
|
|
505
|
+
elems = ", ".join(self._expr(e) for e in node.elements)
|
|
506
|
+
# Single-element tuples need trailing comma: (x,)
|
|
507
|
+
if len(node.elements) == 1:
|
|
508
|
+
elems += ","
|
|
509
|
+
return elems
|
|
510
|
+
|
|
511
|
+
def visit_ListComprehension(self, node):
|
|
512
|
+
elem = self._expr(node.element)
|
|
513
|
+
result = f"[{elem}"
|
|
514
|
+
for clause in self._comprehension_clauses(node):
|
|
515
|
+
target = self._expr(clause.target)
|
|
516
|
+
iterable = self._expr(clause.iterable)
|
|
517
|
+
result += f" for {target} in {iterable}"
|
|
518
|
+
for cond in clause.conditions:
|
|
519
|
+
result += f" if {self._expr(cond)}"
|
|
520
|
+
result += "]"
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
def visit_DictComprehension(self, node):
|
|
524
|
+
key = self._expr(node.key)
|
|
525
|
+
val = self._expr(node.value)
|
|
526
|
+
result = "{" + f"{key}: {val}"
|
|
527
|
+
for clause in self._comprehension_clauses(node):
|
|
528
|
+
target = self._expr(clause.target)
|
|
529
|
+
iterable = self._expr(clause.iterable)
|
|
530
|
+
result += f" for {target} in {iterable}"
|
|
531
|
+
for cond in clause.conditions:
|
|
532
|
+
result += f" if {self._expr(cond)}"
|
|
533
|
+
result += "}"
|
|
534
|
+
return result
|
|
535
|
+
|
|
536
|
+
def visit_GeneratorExpr(self, node):
|
|
537
|
+
elem = self._expr(node.element)
|
|
538
|
+
result = f"({elem}"
|
|
539
|
+
for clause in self._comprehension_clauses(node):
|
|
540
|
+
target = self._expr(clause.target)
|
|
541
|
+
iterable = self._expr(clause.iterable)
|
|
542
|
+
result += f" for {target} in {iterable}"
|
|
543
|
+
for cond in clause.conditions:
|
|
544
|
+
result += f" if {self._expr(cond)}"
|
|
545
|
+
result += ")"
|
|
546
|
+
return result
|
|
547
|
+
|
|
548
|
+
def visit_SetComprehension(self, node):
|
|
549
|
+
elem = self._expr(node.element)
|
|
550
|
+
result = "{" + elem
|
|
551
|
+
for clause in self._comprehension_clauses(node):
|
|
552
|
+
target = self._expr(clause.target)
|
|
553
|
+
iterable = self._expr(clause.iterable)
|
|
554
|
+
result += f" for {target} in {iterable}"
|
|
555
|
+
for cond in clause.conditions:
|
|
556
|
+
result += f" if {self._expr(cond)}"
|
|
557
|
+
result += "}"
|
|
558
|
+
return result
|
|
559
|
+
|
|
560
|
+
def visit_FStringLiteral(self, node):
|
|
561
|
+
result = 'f"'
|
|
562
|
+
for part in node.parts:
|
|
563
|
+
if isinstance(part, str):
|
|
564
|
+
# Escape any double quotes and braces in text
|
|
565
|
+
escaped = part.replace("\\", "\\\\").replace('"', '\\"')
|
|
566
|
+
escaped = escaped.replace("{", "{{").replace("}", "}}")
|
|
567
|
+
result += escaped
|
|
568
|
+
else:
|
|
569
|
+
conversion = getattr(
|
|
570
|
+
part, "fstring_conversion",
|
|
571
|
+
getattr(part, "_fstring_conversion", "")
|
|
572
|
+
)
|
|
573
|
+
format_spec = getattr(
|
|
574
|
+
part, "fstring_format_spec",
|
|
575
|
+
getattr(part, "_fstring_format_spec", "")
|
|
576
|
+
)
|
|
577
|
+
expr_str = self._expr(part)
|
|
578
|
+
suffix = ""
|
|
579
|
+
if conversion:
|
|
580
|
+
suffix += f"!{conversion}"
|
|
581
|
+
if format_spec:
|
|
582
|
+
suffix += f":{format_spec}"
|
|
583
|
+
result += "{" + expr_str + suffix + "}"
|
|
584
|
+
result += '"'
|
|
585
|
+
return result
|
|
586
|
+
|
|
587
|
+
def generic_visit(self, node):
|
|
588
|
+
"""Raise when expression node code generation is not implemented."""
|
|
589
|
+
raise CodeGenerationError(
|
|
590
|
+
f"Unsupported expression node: {type(node).__name__}",
|
|
591
|
+
node.line, node.column
|
|
592
|
+
)
|