multilingualprogramming 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. multilingualprogramming/__init__.py +74 -0
  2. multilingualprogramming/__main__.py +194 -0
  3. multilingualprogramming/codegen/__init__.py +12 -0
  4. multilingualprogramming/codegen/executor.py +215 -0
  5. multilingualprogramming/codegen/python_generator.py +592 -0
  6. multilingualprogramming/codegen/repl.py +489 -0
  7. multilingualprogramming/codegen/runtime_builtins.py +308 -0
  8. multilingualprogramming/core/__init__.py +12 -0
  9. multilingualprogramming/core/ir.py +29 -0
  10. multilingualprogramming/core/lowering.py +24 -0
  11. multilingualprogramming/datetime/__init__.py +11 -0
  12. multilingualprogramming/datetime/date_parser.py +190 -0
  13. multilingualprogramming/datetime/mp_date.py +210 -0
  14. multilingualprogramming/datetime/mp_datetime.py +153 -0
  15. multilingualprogramming/datetime/mp_time.py +147 -0
  16. multilingualprogramming/datetime/resource_loader.py +18 -0
  17. multilingualprogramming/exceptions.py +158 -0
  18. multilingualprogramming/imports.py +150 -0
  19. multilingualprogramming/keyword/__init__.py +13 -0
  20. multilingualprogramming/keyword/keyword_registry.py +249 -0
  21. multilingualprogramming/keyword/keyword_validator.py +59 -0
  22. multilingualprogramming/keyword/language_pack_validator.py +110 -0
  23. multilingualprogramming/lexer/__init__.py +11 -0
  24. multilingualprogramming/lexer/lexer.py +570 -0
  25. multilingualprogramming/lexer/source_reader.py +91 -0
  26. multilingualprogramming/lexer/token.py +54 -0
  27. multilingualprogramming/lexer/token_types.py +38 -0
  28. multilingualprogramming/numeral/__init__.py +11 -0
  29. multilingualprogramming/numeral/abstract_numeral.py +232 -0
  30. multilingualprogramming/numeral/complex_numeral.py +190 -0
  31. multilingualprogramming/numeral/fraction_numeral.py +165 -0
  32. multilingualprogramming/numeral/mp_numeral.py +243 -0
  33. multilingualprogramming/numeral/numeral_converter.py +151 -0
  34. multilingualprogramming/numeral/roman_numeral.py +301 -0
  35. multilingualprogramming/numeral/unicode_numeral.py +292 -0
  36. multilingualprogramming/parser/__init__.py +28 -0
  37. multilingualprogramming/parser/ast_nodes.py +459 -0
  38. multilingualprogramming/parser/ast_printer.py +677 -0
  39. multilingualprogramming/parser/error_messages.py +75 -0
  40. multilingualprogramming/parser/parser.py +1796 -0
  41. multilingualprogramming/parser/semantic_analyzer.py +689 -0
  42. multilingualprogramming/parser/surface_normalizer.py +282 -0
  43. multilingualprogramming/resources/datetime/eras.json +23 -0
  44. multilingualprogramming/resources/datetime/formats.json +32 -0
  45. multilingualprogramming/resources/datetime/months.json +150 -0
  46. multilingualprogramming/resources/datetime/weekdays.json +90 -0
  47. multilingualprogramming/resources/parser/error_messages.json +310 -0
  48. multilingualprogramming/resources/repl/commands.json +636 -0
  49. multilingualprogramming/resources/usm/builtins_aliases.json +731 -0
  50. multilingualprogramming/resources/usm/keywords.json +1063 -0
  51. multilingualprogramming/resources/usm/operators.json +532 -0
  52. multilingualprogramming/resources/usm/schema.json +34 -0
  53. multilingualprogramming/resources/usm/surface_patterns.json +1523 -0
  54. multilingualprogramming/unicode_string.py +140 -0
  55. multilingualprogramming/version.py +9 -0
  56. multilingualprogramming-0.2.0.dist-info/METADATA +350 -0
  57. multilingualprogramming-0.2.0.dist-info/RECORD +61 -0
  58. multilingualprogramming-0.2.0.dist-info/WHEEL +5 -0
  59. multilingualprogramming-0.2.0.dist-info/entry_points.txt +3 -0
  60. multilingualprogramming-0.2.0.dist-info/licenses/LICENSE +674 -0
  61. multilingualprogramming-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ )