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,689 @@
1
+ #
2
+ # SPDX-FileCopyrightText: 2024 John Samuel <johnsamuelwrites@gmail.com>
3
+ #
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ #
6
+
7
+ """Semantic analyzer for the multilingual programming language AST."""
8
+
9
+ from multilingualprogramming.parser.ast_nodes import (
10
+ Identifier, TupleLiteral, StarredExpr,
11
+ )
12
+ from multilingualprogramming.parser.error_messages import ErrorMessageRegistry
13
+ from multilingualprogramming.exceptions import SemanticError
14
+
15
+
16
+ class Symbol:
17
+ """Represents a declared name in a scope."""
18
+
19
+ def __init__(self, name, symbol_type, is_const=False,
20
+ data_type=None, line=0, column=0):
21
+ self.name = name
22
+ self.symbol_type = symbol_type # "variable", "function", "class", "parameter"
23
+ self.is_const = is_const
24
+ self.data_type = data_type
25
+ self.line = line
26
+ self.column = column
27
+
28
+ def __repr__(self):
29
+ return (f"Symbol({self.name!r}, {self.symbol_type!r}, "
30
+ f"const={self.is_const}, type={self.data_type!r})")
31
+
32
+
33
+ class Scope:
34
+ """A single scope level in the scope chain."""
35
+
36
+ def __init__(self, name, scope_type, parent=None):
37
+ self.name = name
38
+ self.scope_type = scope_type # "global", "function", "class", "block"
39
+ self.parent = parent
40
+ self.symbols = {}
41
+
42
+ def define(self, symbol):
43
+ """Define a symbol in this scope."""
44
+ self.symbols[symbol.name] = symbol
45
+
46
+ def lookup(self, name):
47
+ """Look up a symbol, searching parent scopes."""
48
+ if name in self.symbols:
49
+ return self.symbols[name]
50
+ if self.parent:
51
+ return self.parent.lookup(name)
52
+ return None
53
+
54
+ def lookup_local(self, name):
55
+ """Look up in this scope only."""
56
+ return self.symbols.get(name)
57
+
58
+
59
+ class SymbolTable:
60
+ """Manages the scope chain during semantic analysis."""
61
+
62
+ def __init__(self):
63
+ self.global_scope = Scope("global", "global")
64
+ self.current_scope = self.global_scope
65
+
66
+ def enter_scope(self, name, scope_type):
67
+ """Push a new scope."""
68
+ new_scope = Scope(name, scope_type, parent=self.current_scope)
69
+ self.current_scope = new_scope
70
+ return new_scope
71
+
72
+ def exit_scope(self):
73
+ """Pop back to the parent scope."""
74
+ if self.current_scope.parent:
75
+ self.current_scope = self.current_scope.parent
76
+
77
+ def define(self, name, symbol_type, is_const=False,
78
+ data_type=None, line=0, column=0):
79
+ """Define a symbol in the current scope."""
80
+ symbol = Symbol(name, symbol_type, is_const, data_type, line, column)
81
+ self.current_scope.define(symbol)
82
+ return symbol
83
+
84
+ def lookup(self, name):
85
+ """Look up from current scope upward."""
86
+ return self.current_scope.lookup(name)
87
+
88
+ def lookup_local(self, name):
89
+ """Look up in current scope only."""
90
+ return self.current_scope.lookup_local(name)
91
+
92
+
93
+ # pylint: disable=too-many-public-methods
94
+ class SemanticAnalyzer:
95
+ """
96
+ Walks the AST and performs semantic analysis:
97
+ - Scope resolution
98
+ - Constant reassignment detection
99
+ - Break/continue/return context validation
100
+ - Basic type inference
101
+ """
102
+
103
+ def __init__(self, source_language="en"):
104
+ self.symbol_table = SymbolTable()
105
+ self.source_language = source_language
106
+ self.errors = []
107
+ self._in_loop = 0
108
+ self._in_function = 0
109
+ self._in_async_function = 0
110
+ self._error_registry = ErrorMessageRegistry()
111
+
112
+ def analyze(self, program):
113
+ """Analyze the AST. Returns list of SemanticError."""
114
+ self.errors = []
115
+ program.accept(self)
116
+ return self.errors
117
+
118
+ def _validate_parameters(self, params):
119
+ """Validate Python-like parameter ordering and uniqueness."""
120
+ seen_names = set()
121
+ seen_default = False
122
+ seen_vararg = False
123
+ seen_kwarg = False
124
+
125
+ for param in params:
126
+ if isinstance(param, str):
127
+ continue
128
+
129
+ name = param.name
130
+ if name in seen_names:
131
+ self._report("DUPLICATE_DEFINITION", param, name=name)
132
+ seen_names.add(name)
133
+
134
+ if param.is_kwarg:
135
+ if seen_kwarg or param.default is not None:
136
+ self._report("UNEXPECTED_TOKEN", param,
137
+ token=f"invalid parameter '{name}'")
138
+ seen_kwarg = True
139
+ continue
140
+
141
+ if param.is_vararg:
142
+ if seen_vararg or seen_kwarg or param.default is not None:
143
+ self._report("UNEXPECTED_TOKEN", param,
144
+ token=f"invalid parameter '{name}'")
145
+ seen_vararg = True
146
+ continue
147
+
148
+ if seen_kwarg:
149
+ self._report("UNEXPECTED_TOKEN", param,
150
+ token=f"parameter '{name}' after **kwargs")
151
+
152
+ # Python allows required keyword-only params after *args.
153
+ if not seen_vararg:
154
+ if param.default is None and seen_default:
155
+ self._report("UNEXPECTED_TOKEN", param,
156
+ token=f"non-default parameter '{name}' follows default parameter")
157
+ if param.default is not None:
158
+ seen_default = True
159
+
160
+ def _report(self, message_key, node, **kwargs):
161
+ """Record a semantic error."""
162
+ kwargs.setdefault("line", node.line)
163
+ kwargs.setdefault("column", node.column)
164
+ msg = self._error_registry.format(
165
+ message_key, self.source_language, **kwargs
166
+ )
167
+ self.errors.append(SemanticError(msg, node.line, node.column))
168
+
169
+ # ------------------------------------------------------------------
170
+ # Visitors
171
+ # ------------------------------------------------------------------
172
+
173
+ def visit_Program(self, node):
174
+ for stmt in node.body:
175
+ stmt.accept(self)
176
+
177
+ def visit_VariableDeclaration(self, node):
178
+ node.value.accept(self)
179
+ existing = self.symbol_table.lookup_local(node.name)
180
+ if existing:
181
+ self._report("DUPLICATE_DEFINITION", node, name=node.name)
182
+ self.symbol_table.define(
183
+ node.name, "variable", is_const=node.is_const,
184
+ line=node.line, column=node.column
185
+ )
186
+
187
+ def visit_Assignment(self, node):
188
+ node.value.accept(self)
189
+ # Tuple unpacking: define targets instead of looking them up
190
+ if isinstance(node.target, TupleLiteral):
191
+ self._define_assignment_target(node.target)
192
+ else:
193
+ node.target.accept(self)
194
+ # Check const reassignment
195
+ if hasattr(node.target, 'name'):
196
+ sym = self.symbol_table.lookup(node.target.name)
197
+ if sym and sym.is_const:
198
+ self._report("CONST_REASSIGNMENT", node,
199
+ name=node.target.name)
200
+
201
+ def visit_AnnAssignment(self, node):
202
+ if node.annotation:
203
+ node.annotation.accept(self)
204
+ if node.value:
205
+ node.value.accept(self)
206
+ if isinstance(node.target, Identifier):
207
+ existing = self.symbol_table.lookup(node.target.name)
208
+ if existing is None:
209
+ self.symbol_table.define(
210
+ node.target.name, "variable",
211
+ line=node.target.line, column=node.target.column
212
+ )
213
+ else:
214
+ node.target.accept(self)
215
+
216
+ def _define_assignment_target(self, target):
217
+ """Define variables in a tuple unpacking assignment target."""
218
+ if isinstance(target, Identifier):
219
+ existing = self.symbol_table.lookup(target.name)
220
+ if existing is None:
221
+ self.symbol_table.define(
222
+ target.name, "variable",
223
+ line=target.line, column=target.column
224
+ )
225
+ elif isinstance(target, StarredExpr):
226
+ self._define_assignment_target(target.value)
227
+ elif isinstance(target, TupleLiteral):
228
+ for elem in target.elements:
229
+ self._define_assignment_target(elem)
230
+ else:
231
+ target.accept(self)
232
+
233
+ def visit_ExpressionStatement(self, node):
234
+ node.expression.accept(self)
235
+
236
+ def visit_Identifier(self, node):
237
+ sym = self.symbol_table.lookup(node.name)
238
+ if sym is None:
239
+ self._report("UNDEFINED_NAME", node, name=node.name)
240
+
241
+ def visit_NumeralLiteral(self, _node):
242
+ pass
243
+
244
+ def visit_StringLiteral(self, _node):
245
+ pass
246
+
247
+ def visit_DateLiteral(self, _node):
248
+ pass
249
+
250
+ def visit_BooleanLiteral(self, _node):
251
+ pass
252
+
253
+ def visit_NoneLiteral(self, _node):
254
+ pass
255
+
256
+ def visit_ListLiteral(self, node):
257
+ for elem in node.elements:
258
+ elem.accept(self)
259
+
260
+ def visit_DictLiteral(self, node):
261
+ for entry in node.entries:
262
+ if isinstance(entry, tuple):
263
+ key, value = entry
264
+ key.accept(self)
265
+ value.accept(self)
266
+ else:
267
+ entry.accept(self)
268
+
269
+ def visit_SetLiteral(self, node):
270
+ for elem in node.elements:
271
+ elem.accept(self)
272
+
273
+ def visit_DictUnpackEntry(self, node):
274
+ node.value.accept(self)
275
+
276
+ def visit_BinaryOp(self, node):
277
+ node.left.accept(self)
278
+ node.right.accept(self)
279
+
280
+ def visit_UnaryOp(self, node):
281
+ node.operand.accept(self)
282
+
283
+ def visit_BooleanOp(self, node):
284
+ for val in node.values:
285
+ val.accept(self)
286
+
287
+ def visit_CompareOp(self, node):
288
+ node.left.accept(self)
289
+ for _op, right in node.comparators:
290
+ right.accept(self)
291
+
292
+ def visit_CallExpr(self, node):
293
+ node.func.accept(self)
294
+ for arg in node.args:
295
+ arg.accept(self)
296
+ for _name, val in node.keywords:
297
+ val.accept(self)
298
+
299
+ def visit_AttributeAccess(self, node):
300
+ node.obj.accept(self)
301
+
302
+ def visit_IndexAccess(self, node):
303
+ node.obj.accept(self)
304
+ node.index.accept(self)
305
+
306
+ def visit_SliceExpr(self, node):
307
+ if node.start:
308
+ node.start.accept(self)
309
+ if node.stop:
310
+ node.stop.accept(self)
311
+ if node.step:
312
+ node.step.accept(self)
313
+
314
+ def visit_StarredExpr(self, node):
315
+ node.value.accept(self)
316
+
317
+ def visit_TupleLiteral(self, node):
318
+ for elem in node.elements:
319
+ elem.accept(self)
320
+
321
+ def visit_LambdaExpr(self, node):
322
+ self.symbol_table.enter_scope("lambda", "function")
323
+ self._in_function += 1
324
+ self._validate_parameters(node.params)
325
+ for param in node.params:
326
+ if isinstance(param, str):
327
+ self.symbol_table.define(
328
+ param, "parameter", line=node.line, column=node.column
329
+ )
330
+ else:
331
+ if param.default:
332
+ param.default.accept(self)
333
+ self.symbol_table.define(
334
+ param.name, "parameter",
335
+ line=param.line, column=param.column
336
+ )
337
+ node.body.accept(self)
338
+ self._in_function -= 1
339
+ self.symbol_table.exit_scope()
340
+
341
+ def visit_YieldExpr(self, node):
342
+ if self._in_function == 0:
343
+ self._report("YIELD_OUTSIDE_FUNCTION", node)
344
+ if node.value:
345
+ node.value.accept(self)
346
+
347
+ def visit_AwaitExpr(self, node):
348
+ if self._in_async_function == 0:
349
+ self._report("UNEXPECTED_TOKEN", node, token="await")
350
+ node.value.accept(self)
351
+
352
+ def visit_NamedExpr(self, node):
353
+ node.value.accept(self)
354
+ if isinstance(node.target, Identifier):
355
+ existing = self.symbol_table.lookup(node.target.name)
356
+ if existing is None:
357
+ self.symbol_table.define(
358
+ node.target.name, "variable",
359
+ line=node.target.line, column=node.target.column
360
+ )
361
+ else:
362
+ node.target.accept(self)
363
+
364
+ def visit_ConditionalExpr(self, node):
365
+ node.condition.accept(self)
366
+ node.true_expr.accept(self)
367
+ node.false_expr.accept(self)
368
+
369
+ # -- Simple statements --
370
+
371
+ def visit_PassStatement(self, _node):
372
+ pass
373
+
374
+ def visit_ReturnStatement(self, node):
375
+ if self._in_function == 0:
376
+ self._report("RETURN_OUTSIDE_FUNCTION", node)
377
+ if node.value:
378
+ node.value.accept(self)
379
+
380
+ def visit_BreakStatement(self, node):
381
+ if self._in_loop == 0:
382
+ self._report("BREAK_OUTSIDE_LOOP", node)
383
+
384
+ def visit_ContinueStatement(self, node):
385
+ if self._in_loop == 0:
386
+ self._report("CONTINUE_OUTSIDE_LOOP", node)
387
+
388
+ def visit_RaiseStatement(self, node):
389
+ if node.value:
390
+ node.value.accept(self)
391
+ if getattr(node, "cause", None):
392
+ node.cause.accept(self)
393
+
394
+ def visit_DelStatement(self, node):
395
+ node.target.accept(self)
396
+
397
+ def visit_AssertStatement(self, node):
398
+ node.test.accept(self)
399
+ if node.msg:
400
+ node.msg.accept(self)
401
+
402
+ def visit_ChainedAssignment(self, node):
403
+ node.value.accept(self)
404
+ for target in node.targets:
405
+ if isinstance(target, Identifier):
406
+ existing = self.symbol_table.lookup(target.name)
407
+ if existing is None:
408
+ self.symbol_table.define(
409
+ target.name, "variable",
410
+ line=target.line, column=target.column
411
+ )
412
+ elif isinstance(target, TupleLiteral):
413
+ self._define_assignment_target(target)
414
+ else:
415
+ target.accept(self)
416
+
417
+ def visit_GlobalStatement(self, node):
418
+ for name in node.names:
419
+ # Define in current scope so references don't trigger "undefined"
420
+ self.symbol_table.define(
421
+ name, "variable", line=node.line, column=node.column
422
+ )
423
+
424
+ def visit_LocalStatement(self, node):
425
+ for name in node.names:
426
+ # Define in current scope so references don't trigger "undefined"
427
+ self.symbol_table.define(
428
+ name, "variable", line=node.line, column=node.column
429
+ )
430
+
431
+ def visit_YieldStatement(self, node):
432
+ if self._in_function == 0:
433
+ self._report("YIELD_OUTSIDE_FUNCTION", node)
434
+ if node.value:
435
+ node.value.accept(self)
436
+
437
+ # -- Compound statements --
438
+
439
+ def visit_IfStatement(self, node):
440
+ node.condition.accept(self)
441
+ for stmt in node.body:
442
+ stmt.accept(self)
443
+ for elif_cond, elif_body in node.elif_clauses:
444
+ elif_cond.accept(self)
445
+ for stmt in elif_body:
446
+ stmt.accept(self)
447
+ if node.else_body:
448
+ for stmt in node.else_body:
449
+ stmt.accept(self)
450
+
451
+ def visit_WhileLoop(self, node):
452
+ node.condition.accept(self)
453
+ self._in_loop += 1
454
+ for stmt in node.body:
455
+ stmt.accept(self)
456
+ self._in_loop -= 1
457
+ if node.else_body:
458
+ for stmt in node.else_body:
459
+ stmt.accept(self)
460
+
461
+ def visit_ForLoop(self, node):
462
+ if getattr(node, "is_async", False) and self._in_async_function == 0:
463
+ self._report("UNEXPECTED_TOKEN", node, token="async for")
464
+ node.iterable.accept(self)
465
+ self._define_for_target(node.target)
466
+ self._in_loop += 1
467
+ for stmt in node.body:
468
+ stmt.accept(self)
469
+ self._in_loop -= 1
470
+ if getattr(node, "else_body", None):
471
+ for stmt in node.else_body:
472
+ stmt.accept(self)
473
+
474
+ def _define_for_target(self, target):
475
+ """Define for-loop target variable(s) in the current scope."""
476
+ if isinstance(target, TupleLiteral):
477
+ for elem in target.elements:
478
+ self._define_for_target(elem)
479
+ elif isinstance(target, StarredExpr):
480
+ self._define_for_target(target.value)
481
+ else:
482
+ self.symbol_table.define(
483
+ target.name, "variable",
484
+ line=target.line, column=target.column
485
+ )
486
+
487
+ def visit_FunctionDef(self, node):
488
+ # Visit decorators
489
+ for dec in getattr(node, 'decorators', []):
490
+ dec.accept(self)
491
+ self.symbol_table.define(
492
+ node.name, "function", line=node.line, column=node.column
493
+ )
494
+ self.symbol_table.enter_scope(node.name, "function")
495
+ self._in_function += 1
496
+ if getattr(node, "is_async", False):
497
+ self._in_async_function += 1
498
+ self._validate_parameters(node.params)
499
+ for param in node.params:
500
+ if isinstance(param, str):
501
+ self.symbol_table.define(
502
+ param, "parameter", line=node.line, column=node.column
503
+ )
504
+ else:
505
+ # Parameter node
506
+ if getattr(param, "annotation", None):
507
+ param.annotation.accept(self)
508
+ if param.default:
509
+ param.default.accept(self)
510
+ self.symbol_table.define(
511
+ param.name, "parameter",
512
+ line=param.line, column=param.column
513
+ )
514
+ if getattr(node, "return_annotation", None):
515
+ node.return_annotation.accept(self)
516
+ for stmt in node.body:
517
+ stmt.accept(self)
518
+ if getattr(node, "is_async", False):
519
+ self._in_async_function -= 1
520
+ self._in_function -= 1
521
+ self.symbol_table.exit_scope()
522
+
523
+ def visit_ClassDef(self, node):
524
+ # Visit decorators
525
+ for dec in getattr(node, 'decorators', []):
526
+ dec.accept(self)
527
+ self.symbol_table.define(
528
+ node.name, "class", line=node.line, column=node.column
529
+ )
530
+ self.symbol_table.enter_scope(node.name, "class")
531
+ for base in node.bases:
532
+ base.accept(self)
533
+ for stmt in node.body:
534
+ stmt.accept(self)
535
+ self.symbol_table.exit_scope()
536
+
537
+ def visit_TryStatement(self, node):
538
+ for stmt in node.body:
539
+ stmt.accept(self)
540
+ for handler in node.handlers:
541
+ handler.accept(self)
542
+ if node.else_body:
543
+ for stmt in node.else_body:
544
+ stmt.accept(self)
545
+ if node.finally_body:
546
+ for stmt in node.finally_body:
547
+ stmt.accept(self)
548
+
549
+ def visit_ExceptHandler(self, node):
550
+ self.symbol_table.enter_scope("except", "block")
551
+ if node.exc_type:
552
+ node.exc_type.accept(self)
553
+ if node.name:
554
+ self.symbol_table.define(
555
+ node.name, "variable",
556
+ line=node.line, column=node.column
557
+ )
558
+ for stmt in node.body:
559
+ stmt.accept(self)
560
+ self.symbol_table.exit_scope()
561
+
562
+ def visit_MatchStatement(self, node):
563
+ node.subject.accept(self)
564
+ for case in node.cases:
565
+ case.accept(self)
566
+
567
+ def visit_CaseClause(self, node):
568
+ if node.pattern:
569
+ node.pattern.accept(self)
570
+ if getattr(node, "guard", None):
571
+ node.guard.accept(self)
572
+ for stmt in node.body:
573
+ stmt.accept(self)
574
+
575
+ def visit_WithStatement(self, node):
576
+ if getattr(node, "is_async", False) and self._in_async_function == 0:
577
+ self._report("UNEXPECTED_TOKEN", node, token="async with")
578
+ for context_expr, _name in node.items:
579
+ context_expr.accept(self)
580
+ self.symbol_table.enter_scope("with", "block")
581
+ for _context_expr, name in node.items:
582
+ if name:
583
+ self.symbol_table.define(
584
+ name, "variable",
585
+ line=node.line, column=node.column
586
+ )
587
+ for stmt in node.body:
588
+ stmt.accept(self)
589
+ self.symbol_table.exit_scope()
590
+
591
+ def visit_ListComprehension(self, node):
592
+ self.symbol_table.enter_scope("listcomp", "block")
593
+ for clause in getattr(node, "clauses", [node]):
594
+ clause.iterable.accept(self)
595
+ if isinstance(clause.target, str):
596
+ self.symbol_table.define(
597
+ clause.target, "variable",
598
+ line=node.line, column=node.column
599
+ )
600
+ else:
601
+ self._define_comp_target(clause.target, node)
602
+ for cond in clause.conditions:
603
+ cond.accept(self)
604
+ node.element.accept(self)
605
+ self.symbol_table.exit_scope()
606
+
607
+ def visit_DictComprehension(self, node):
608
+ self.symbol_table.enter_scope("dictcomp", "block")
609
+ for clause in getattr(node, "clauses", [node]):
610
+ clause.iterable.accept(self)
611
+ if isinstance(clause.target, str):
612
+ self.symbol_table.define(
613
+ clause.target, "variable",
614
+ line=node.line, column=node.column
615
+ )
616
+ else:
617
+ self._define_comp_target(clause.target, node)
618
+ for cond in clause.conditions:
619
+ cond.accept(self)
620
+ node.key.accept(self)
621
+ node.value.accept(self)
622
+ self.symbol_table.exit_scope()
623
+
624
+ def visit_GeneratorExpr(self, node):
625
+ self.symbol_table.enter_scope("genexpr", "block")
626
+ for clause in getattr(node, "clauses", [node]):
627
+ clause.iterable.accept(self)
628
+ if isinstance(clause.target, str):
629
+ self.symbol_table.define(
630
+ clause.target, "variable",
631
+ line=node.line, column=node.column
632
+ )
633
+ else:
634
+ self._define_comp_target(clause.target, node)
635
+ for cond in clause.conditions:
636
+ cond.accept(self)
637
+ node.element.accept(self)
638
+ self.symbol_table.exit_scope()
639
+
640
+ def visit_SetComprehension(self, node):
641
+ self.symbol_table.enter_scope("setcomp", "block")
642
+ for clause in getattr(node, "clauses", [node]):
643
+ clause.iterable.accept(self)
644
+ if isinstance(clause.target, str):
645
+ self.symbol_table.define(
646
+ clause.target, "variable",
647
+ line=node.line, column=node.column
648
+ )
649
+ else:
650
+ self._define_comp_target(clause.target, node)
651
+ for cond in clause.conditions:
652
+ cond.accept(self)
653
+ node.element.accept(self)
654
+ self.symbol_table.exit_scope()
655
+
656
+ def _define_comp_target(self, target, node):
657
+ """Define comprehension target variable(s) in current scope."""
658
+ if isinstance(target, Identifier):
659
+ self.symbol_table.define(
660
+ target.name, "variable",
661
+ line=target.line, column=target.column
662
+ )
663
+ elif isinstance(target, StarredExpr):
664
+ self._define_comp_target(target.value, node)
665
+ elif isinstance(target, TupleLiteral):
666
+ for elem in target.elements:
667
+ self._define_comp_target(elem, node)
668
+
669
+ def visit_FStringLiteral(self, node):
670
+ for part in node.parts:
671
+ if not isinstance(part, str):
672
+ part.accept(self)
673
+
674
+ def visit_ImportStatement(self, node):
675
+ name = node.alias or node.module
676
+ self.symbol_table.define(
677
+ name, "variable", line=node.line, column=node.column
678
+ )
679
+
680
+ def visit_FromImportStatement(self, node):
681
+ for name, alias in node.names:
682
+ sym_name = alias or name
683
+ self.symbol_table.define(
684
+ sym_name, "variable", line=node.line, column=node.column
685
+ )
686
+
687
+ def generic_visit(self, _node):
688
+ """Ignore unsupported nodes during semantic traversal."""
689
+ return None