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,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
|