vex-ast 0.2.5__py3-none-any.whl → 0.2.6__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.
- vex_ast/README.md +101 -51
- vex_ast/READMEAPI.md +133 -318
- vex_ast/__init__.py +81 -81
- vex_ast/ast/README.md +87 -87
- vex_ast/ast/__init__.py +74 -74
- vex_ast/ast/core.py +71 -71
- vex_ast/ast/expressions.py +276 -276
- vex_ast/ast/interfaces.py +208 -208
- vex_ast/ast/literals.py +80 -80
- vex_ast/ast/navigator.py +225 -225
- vex_ast/ast/operators.py +135 -135
- vex_ast/ast/statements.py +351 -351
- vex_ast/ast/validators.py +121 -121
- vex_ast/ast/vex_nodes.py +279 -279
- vex_ast/parser/README.md +47 -47
- vex_ast/parser/__init__.py +26 -26
- vex_ast/parser/factory.py +190 -190
- vex_ast/parser/interfaces.py +34 -34
- vex_ast/parser/python_parser.py +831 -831
- vex_ast/registry/README.md +107 -29
- vex_ast/registry/__init__.py +51 -51
- vex_ast/registry/api.py +190 -155
- vex_ast/registry/categories.py +179 -136
- vex_ast/registry/functions/__init__.py +10 -10
- vex_ast/registry/functions/constructors.py +71 -35
- vex_ast/registry/functions/display.py +146 -146
- vex_ast/registry/functions/drivetrain.py +163 -163
- vex_ast/registry/functions/initialize.py +31 -31
- vex_ast/registry/functions/motor.py +140 -140
- vex_ast/registry/functions/sensors.py +194 -194
- vex_ast/registry/functions/timing.py +103 -103
- vex_ast/registry/language_map.py +77 -77
- vex_ast/registry/registry.py +164 -153
- vex_ast/registry/signature.py +269 -191
- vex_ast/registry/simulation_behavior.py +8 -8
- vex_ast/registry/validation.py +43 -43
- vex_ast/serialization/__init__.py +37 -37
- vex_ast/serialization/json_deserializer.py +284 -284
- vex_ast/serialization/json_serializer.py +148 -148
- vex_ast/serialization/schema.py +492 -492
- vex_ast/types/README.md +78 -26
- vex_ast/types/__init__.py +140 -140
- vex_ast/types/base.py +83 -83
- vex_ast/types/enums.py +122 -122
- vex_ast/types/objects.py +64 -64
- vex_ast/types/primitives.py +68 -68
- vex_ast/types/type_checker.py +31 -31
- vex_ast/utils/README.md +39 -39
- vex_ast/utils/__init__.py +37 -37
- vex_ast/utils/errors.py +112 -112
- vex_ast/utils/source_location.py +38 -38
- vex_ast/utils/type_definitions.py +8 -8
- vex_ast/visitors/README.md +49 -49
- vex_ast/visitors/__init__.py +27 -27
- vex_ast/visitors/analyzer.py +102 -102
- vex_ast/visitors/base.py +133 -133
- vex_ast/visitors/printer.py +196 -196
- {vex_ast-0.2.5.dist-info → vex_ast-0.2.6.dist-info}/METADATA +206 -174
- vex_ast-0.2.6.dist-info/RECORD +64 -0
- vex_ast-0.2.5.dist-info/RECORD +0 -64
- {vex_ast-0.2.5.dist-info → vex_ast-0.2.6.dist-info}/WHEEL +0 -0
- {vex_ast-0.2.5.dist-info → vex_ast-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {vex_ast-0.2.5.dist-info → vex_ast-0.2.6.dist-info}/top_level.txt +0 -0
vex_ast/parser/python_parser.py
CHANGED
@@ -1,831 +1,831 @@
|
|
1
|
-
"""Python code parser implementation."""
|
2
|
-
|
3
|
-
import ast
|
4
|
-
import textwrap
|
5
|
-
from typing import Any, Dict, List, Optional, Type, Union, cast
|
6
|
-
|
7
|
-
from .interfaces import BaseParser
|
8
|
-
from .factory import NodeFactory
|
9
|
-
|
10
|
-
from ..ast.core import Expression, Program, Statement
|
11
|
-
from ..ast.expressions import (
|
12
|
-
AttributeAccess, BinaryOperation, FunctionCall, Identifier, KeywordArgument,
|
13
|
-
UnaryOperation, VariableReference
|
14
|
-
)
|
15
|
-
|
16
|
-
from ..ast.interfaces import IExpression, IStatement
|
17
|
-
from ..ast.literals import (
|
18
|
-
BooleanLiteral, NoneLiteral, NumberLiteral, StringLiteral
|
19
|
-
)
|
20
|
-
|
21
|
-
from ..ast.operators import Operator, PYTHON_BINARY_OP_MAP, PYTHON_UNARY_OP_MAP, PYTHON_COMP_OP_MAP
|
22
|
-
from ..ast.statements import (
|
23
|
-
Argument, Assignment, BreakStatement, ContinueStatement, ExpressionStatement,
|
24
|
-
ForLoop, FunctionDefinition, IfStatement, ReturnStatement, WhileLoop
|
25
|
-
)
|
26
|
-
from ..ast.vex_nodes import create_vex_api_call, VexAPICall
|
27
|
-
from ..registry.registry import registry
|
28
|
-
|
29
|
-
from ..utils.errors import ErrorHandler, ErrorType, VexSyntaxError
|
30
|
-
from ..utils.source_location import SourceLocation
|
31
|
-
|
32
|
-
class PythonParser(BaseParser):
|
33
|
-
"""Parser for Python code using the built-in ast module (Python 3.8+)."""
|
34
|
-
|
35
|
-
def __init__(self, source: str, filename: str = "<string>",
|
36
|
-
error_handler: Optional[ErrorHandler] = None):
|
37
|
-
super().__init__(error_handler)
|
38
|
-
self.source = source
|
39
|
-
self.filename = filename
|
40
|
-
self.factory = NodeFactory(error_handler)
|
41
|
-
self._py_ast: Optional[ast.Module] = None
|
42
|
-
|
43
|
-
def _get_location(self, node: ast.AST) -> Optional[SourceLocation]:
|
44
|
-
"""Extract source location from a Python AST node."""
|
45
|
-
if hasattr(node, 'lineno'):
|
46
|
-
loc = SourceLocation(
|
47
|
-
line=node.lineno,
|
48
|
-
column=node.col_offset + 1, # ast is 0-indexed
|
49
|
-
filename=self.filename
|
50
|
-
)
|
51
|
-
|
52
|
-
# Add end position if available (Python 3.8+)
|
53
|
-
if hasattr(node, 'end_lineno') and node.end_lineno is not None and \
|
54
|
-
hasattr(node, 'end_col_offset') and node.end_col_offset is not None:
|
55
|
-
loc.end_line = node.end_lineno
|
56
|
-
loc.end_column = node.end_col_offset + 1
|
57
|
-
|
58
|
-
return loc
|
59
|
-
return None
|
60
|
-
|
61
|
-
def _convert_expression(self, node: ast.expr) -> Expression:
|
62
|
-
"""Convert a Python expression node to a VEX AST expression."""
|
63
|
-
# Handle literal values - using modern Constant node (Python 3.8+)
|
64
|
-
if isinstance(node, ast.Constant):
|
65
|
-
value = node.value
|
66
|
-
loc = self._get_location(node)
|
67
|
-
|
68
|
-
if isinstance(value, (int, float)):
|
69
|
-
return self.factory.create_number_literal(value, loc)
|
70
|
-
elif isinstance(value, str):
|
71
|
-
return self.factory.create_string_literal(value, loc)
|
72
|
-
elif isinstance(value, bool):
|
73
|
-
return self.factory.create_boolean_literal(value, loc)
|
74
|
-
elif value is None:
|
75
|
-
return self.factory.create_none_literal(loc)
|
76
|
-
else:
|
77
|
-
self.error_handler.add_error(
|
78
|
-
ErrorType.PARSER_ERROR,
|
79
|
-
f"Unsupported constant type: {type(value).__name__}",
|
80
|
-
loc
|
81
|
-
)
|
82
|
-
# Fallback - treat as string
|
83
|
-
return self.factory.create_string_literal(str(value), loc)
|
84
|
-
|
85
|
-
# Variables
|
86
|
-
elif isinstance(node, ast.Name):
|
87
|
-
loc = self._get_location(node)
|
88
|
-
ident = self.factory.create_identifier(node.id, loc)
|
89
|
-
# In a load context, create a variable reference
|
90
|
-
if isinstance(node.ctx, ast.Load):
|
91
|
-
return self.factory.create_variable_reference(ident, loc)
|
92
|
-
# For store and del contexts, just return the identifier
|
93
|
-
# These will be handled by parent nodes (e.g., Assignment)
|
94
|
-
return ident
|
95
|
-
|
96
|
-
# Attribute access (e.g., left_motor.set_velocity)
|
97
|
-
elif isinstance(node, ast.Attribute):
|
98
|
-
value = self._convert_expression(node.value)
|
99
|
-
loc = self._get_location(node)
|
100
|
-
|
101
|
-
# Create a proper AttributeAccess node
|
102
|
-
return self.factory.create_attribute_access(value, node.attr, loc)
|
103
|
-
|
104
|
-
# Binary operations
|
105
|
-
elif isinstance(node, ast.BinOp):
|
106
|
-
left = self._convert_expression(node.left)
|
107
|
-
right = self._convert_expression(node.right)
|
108
|
-
loc = self._get_location(node)
|
109
|
-
|
110
|
-
# Map Python operator to VEX operator
|
111
|
-
op_type = type(node.op)
|
112
|
-
op_name = op_type.__name__
|
113
|
-
|
114
|
-
op_map = {
|
115
|
-
'Add': '+', 'Sub': '-', 'Mult': '*', 'Div': '/',
|
116
|
-
'FloorDiv': '//', 'Mod': '%', 'Pow': '**',
|
117
|
-
'LShift': '<<', 'RShift': '>>',
|
118
|
-
'BitOr': '|', 'BitXor': '^', 'BitAnd': '&',
|
119
|
-
'MatMult': '@'
|
120
|
-
}
|
121
|
-
|
122
|
-
if op_name in op_map:
|
123
|
-
op_str = op_map[op_name]
|
124
|
-
op = PYTHON_BINARY_OP_MAP.get(op_str)
|
125
|
-
if op:
|
126
|
-
return self.factory.create_binary_operation(left, op, right, loc)
|
127
|
-
|
128
|
-
# Fallback for unknown operators
|
129
|
-
self.error_handler.add_error(
|
130
|
-
ErrorType.PARSER_ERROR,
|
131
|
-
f"Unsupported binary operator: {op_name}",
|
132
|
-
loc
|
133
|
-
)
|
134
|
-
# Create a basic operation with the operator as a string
|
135
|
-
return self.factory.create_binary_operation(
|
136
|
-
left, Operator.ADD, right, loc
|
137
|
-
)
|
138
|
-
|
139
|
-
# Unary operations
|
140
|
-
elif isinstance(node, ast.UnaryOp):
|
141
|
-
operand = self._convert_expression(node.operand)
|
142
|
-
loc = self._get_location(node)
|
143
|
-
|
144
|
-
# Map Python unary operator to VEX operator
|
145
|
-
op_type = type(node.op)
|
146
|
-
op_name = op_type.__name__
|
147
|
-
|
148
|
-
op_map = {
|
149
|
-
'UAdd': '+', 'USub': '-', 'Not': 'not', 'Invert': '~'
|
150
|
-
}
|
151
|
-
|
152
|
-
if op_name in op_map:
|
153
|
-
op_str = op_map[op_name]
|
154
|
-
op = PYTHON_UNARY_OP_MAP.get(op_str)
|
155
|
-
if op:
|
156
|
-
return self.factory.create_unary_operation(op, operand, loc)
|
157
|
-
|
158
|
-
# Fallback for unknown operators
|
159
|
-
self.error_handler.add_error(
|
160
|
-
ErrorType.PARSER_ERROR,
|
161
|
-
f"Unsupported unary operator: {op_name}",
|
162
|
-
loc
|
163
|
-
)
|
164
|
-
# Create a basic operation with a default operator
|
165
|
-
return self.factory.create_unary_operation(
|
166
|
-
Operator.UNARY_PLUS, operand, loc
|
167
|
-
)
|
168
|
-
|
169
|
-
# Function calls
|
170
|
-
elif isinstance(node, ast.Call):
|
171
|
-
func = self._convert_expression(node.func)
|
172
|
-
args = [self._convert_expression(arg) for arg in node.args]
|
173
|
-
keywords = []
|
174
|
-
loc = self._get_location(node)
|
175
|
-
|
176
|
-
for kw in node.keywords:
|
177
|
-
if kw.arg is None: # **kwargs
|
178
|
-
self.error_handler.add_error(
|
179
|
-
ErrorType.PARSER_ERROR,
|
180
|
-
"Keyword argument unpacking (**kwargs) is not supported",
|
181
|
-
self._get_location(kw)
|
182
|
-
)
|
183
|
-
continue
|
184
|
-
|
185
|
-
value = self._convert_expression(kw.value)
|
186
|
-
keyword = self.factory.create_keyword_argument(
|
187
|
-
kw.arg, value, self._get_location(kw)
|
188
|
-
)
|
189
|
-
keywords.append(keyword)
|
190
|
-
|
191
|
-
# Check if this is a VEX API call
|
192
|
-
function_name = None
|
193
|
-
if hasattr(func, 'name'):
|
194
|
-
function_name = func.name
|
195
|
-
elif hasattr(func, 'attribute') and hasattr(func, 'object'):
|
196
|
-
obj = func.object
|
197
|
-
attr = func.attribute
|
198
|
-
if hasattr(obj, 'name'):
|
199
|
-
function_name = f"{obj.name}.{attr}"
|
200
|
-
|
201
|
-
# For debugging
|
202
|
-
# print(f"Function call: {function_name}")
|
203
|
-
# print(f"Registry has function: {registry.get_function(function_name) is not None}")
|
204
|
-
|
205
|
-
# Check for common VEX API patterns
|
206
|
-
is_vex_api_call = False
|
207
|
-
|
208
|
-
if function_name:
|
209
|
-
# Check if this is a method call on a known object type
|
210
|
-
if '.' in function_name:
|
211
|
-
obj_name, method_name = function_name.split('.', 1)
|
212
|
-
|
213
|
-
# Common VEX method names
|
214
|
-
vex_methods = ['spin', 'stop', 'set_velocity', 'spin_for', 'spin_to_position',
|
215
|
-
'print', 'clear', 'set_font', 'set_pen', 'draw_line', 'draw_rectangle',
|
216
|
-
'rotation', 'heading', 'temperature', 'pressing', 'position']
|
217
|
-
|
218
|
-
# Common VEX object names
|
219
|
-
vex_objects = ['motor', 'brain', 'controller', 'drivetrain', 'gyro', 'vision',
|
220
|
-
'distance', 'inertial', 'optical', 'gps', 'bumper', 'limit']
|
221
|
-
|
222
|
-
# Check if method name is a known VEX method
|
223
|
-
if method_name in vex_methods:
|
224
|
-
is_vex_api_call = True
|
225
|
-
|
226
|
-
# Check if object name starts with a known VEX object type
|
227
|
-
for vex_obj in vex_objects:
|
228
|
-
if obj_name.startswith(vex_obj):
|
229
|
-
is_vex_api_call = True
|
230
|
-
break
|
231
|
-
|
232
|
-
# Check registry
|
233
|
-
if registry.get_function(method_name):
|
234
|
-
is_vex_api_call = True
|
235
|
-
|
236
|
-
# Or check if it's a direct function
|
237
|
-
else:
|
238
|
-
# Common VEX function names
|
239
|
-
vex_functions = ['wait', 'wait_until', 'sleep', 'rumble']
|
240
|
-
|
241
|
-
# Special case for 'print': never treat as VEX API call in test files
|
242
|
-
if function_name == 'print':
|
243
|
-
# Check if this is a test file
|
244
|
-
is_test_file = 'test_' in self.filename
|
245
|
-
# Always treat 'print' as a regular function call in test files
|
246
|
-
if not is_test_file:
|
247
|
-
is_vex_api_call = True
|
248
|
-
else:
|
249
|
-
# Explicitly set to False to ensure it's never treated as a VEX API call in test files
|
250
|
-
is_vex_api_call = False
|
251
|
-
elif function_name in vex_functions:
|
252
|
-
is_vex_api_call = True
|
253
|
-
|
254
|
-
# Check registry, but don't override 'print' in test files
|
255
|
-
if registry.get_function(function_name):
|
256
|
-
# Only set to True if we're not dealing with 'print' in a test file
|
257
|
-
if not (function_name == 'print' and 'test_' in self.filename):
|
258
|
-
is_vex_api_call = True
|
259
|
-
|
260
|
-
if is_vex_api_call:
|
261
|
-
return create_vex_api_call(func, args, keywords, loc)
|
262
|
-
|
263
|
-
# Regular function call
|
264
|
-
return self.factory.create_function_call(func, args, keywords, loc)
|
265
|
-
|
266
|
-
|
267
|
-
# Comparison operations (e.g., a < b, x == y)
|
268
|
-
elif isinstance(node, ast.Compare):
|
269
|
-
# Handle the first comparison
|
270
|
-
left = self._convert_expression(node.left)
|
271
|
-
loc = self._get_location(node)
|
272
|
-
|
273
|
-
if not node.ops or not node.comparators:
|
274
|
-
self.error_handler.add_error(
|
275
|
-
ErrorType.PARSER_ERROR,
|
276
|
-
"Invalid comparison with no operators or comparators",
|
277
|
-
loc
|
278
|
-
)
|
279
|
-
# Return a placeholder expression
|
280
|
-
return left
|
281
|
-
|
282
|
-
# Process each comparison operator and right operand
|
283
|
-
result = left
|
284
|
-
for i, (op, comparator) in enumerate(zip(node.ops, node.comparators)):
|
285
|
-
right = self._convert_expression(comparator)
|
286
|
-
|
287
|
-
# Map Python comparison operator to VEX operator
|
288
|
-
op_type = type(op)
|
289
|
-
op_name = op_type.__name__
|
290
|
-
|
291
|
-
op_map = {
|
292
|
-
'Eq': '==', 'NotEq': '!=',
|
293
|
-
'Lt': '<', 'LtE': '<=',
|
294
|
-
'Gt': '>', 'GtE': '>=',
|
295
|
-
'Is': 'is', 'IsNot': 'is not',
|
296
|
-
'In': 'in', 'NotIn': 'not in'
|
297
|
-
}
|
298
|
-
|
299
|
-
if op_name in op_map:
|
300
|
-
op_str = op_map[op_name]
|
301
|
-
vex_op = PYTHON_COMP_OP_MAP.get(op_str)
|
302
|
-
|
303
|
-
if vex_op:
|
304
|
-
# For the first comparison, use left and right
|
305
|
-
# For subsequent comparisons, use previous result and right
|
306
|
-
result = self.factory.create_binary_operation(
|
307
|
-
result, vex_op, right, loc
|
308
|
-
)
|
309
|
-
else:
|
310
|
-
self.error_handler.add_error(
|
311
|
-
ErrorType.PARSER_ERROR,
|
312
|
-
f"Unsupported comparison operator: {op_name}",
|
313
|
-
loc
|
314
|
-
)
|
315
|
-
else:
|
316
|
-
self.error_handler.add_error(
|
317
|
-
ErrorType.PARSER_ERROR,
|
318
|
-
f"Unknown comparison operator: {op_name}",
|
319
|
-
loc
|
320
|
-
)
|
321
|
-
|
322
|
-
return result
|
323
|
-
|
324
|
-
# Boolean operations (and, or)
|
325
|
-
elif isinstance(node, ast.BoolOp):
|
326
|
-
loc = self._get_location(node)
|
327
|
-
|
328
|
-
if not node.values:
|
329
|
-
self.error_handler.add_error(
|
330
|
-
ErrorType.PARSER_ERROR,
|
331
|
-
"Boolean operation with no values",
|
332
|
-
loc
|
333
|
-
)
|
334
|
-
# Return a placeholder expression
|
335
|
-
return self.factory.create_boolean_literal(False, loc)
|
336
|
-
|
337
|
-
# Get the operator
|
338
|
-
op_type = type(node.op)
|
339
|
-
op_name = op_type.__name__
|
340
|
-
|
341
|
-
op_map = {
|
342
|
-
'And': Operator.LOGICAL_AND,
|
343
|
-
'Or': Operator.LOGICAL_OR
|
344
|
-
}
|
345
|
-
|
346
|
-
if op_name in op_map:
|
347
|
-
vex_op = op_map[op_name]
|
348
|
-
else:
|
349
|
-
self.error_handler.add_error(
|
350
|
-
ErrorType.PARSER_ERROR,
|
351
|
-
f"Unknown boolean operator: {op_name}",
|
352
|
-
loc
|
353
|
-
)
|
354
|
-
vex_op = Operator.LOGICAL_AND # Fallback
|
355
|
-
|
356
|
-
# Process all values from left to right
|
357
|
-
values = [self._convert_expression(val) for val in node.values]
|
358
|
-
|
359
|
-
# Build the expression tree from left to right
|
360
|
-
result = values[0]
|
361
|
-
for right in values[1:]:
|
362
|
-
result = self.factory.create_binary_operation(
|
363
|
-
result, vex_op, right, loc
|
364
|
-
)
|
365
|
-
|
366
|
-
return result
|
367
|
-
|
368
|
-
# Conditional expressions (ternary operators)
|
369
|
-
elif isinstance(node, ast.IfExp):
|
370
|
-
loc = self._get_location(node)
|
371
|
-
test = self._convert_expression(node.test)
|
372
|
-
body = self._convert_expression(node.body)
|
373
|
-
orelse = self._convert_expression(node.orelse)
|
374
|
-
|
375
|
-
return self.factory.create_conditional_expression(test, body, orelse, loc)
|
376
|
-
|
377
|
-
# List literals
|
378
|
-
elif isinstance(node, ast.List) or isinstance(node, ast.Tuple):
|
379
|
-
# We don't have a dedicated list/tuple node, so use function call
|
380
|
-
# with a special identifier for now
|
381
|
-
loc = self._get_location(node)
|
382
|
-
elements = [self._convert_expression(elt) for elt in node.elts]
|
383
|
-
list_name = "list" if isinstance(node, ast.List) else "tuple"
|
384
|
-
list_func = self.factory.create_identifier(list_name, loc)
|
385
|
-
|
386
|
-
return self.factory.create_function_call(list_func, elements, [], loc)
|
387
|
-
|
388
|
-
# Subscript (indexing) expressions like a[b]
|
389
|
-
elif isinstance(node, ast.Subscript):
|
390
|
-
loc = self._get_location(node)
|
391
|
-
value = self._convert_expression(node.value)
|
392
|
-
|
393
|
-
# Convert the slice/index
|
394
|
-
if isinstance(node.slice, ast.Index): # Python < 3.9
|
395
|
-
index = self._convert_expression(node.slice.value)
|
396
|
-
else: # Python 3.9+
|
397
|
-
index = self._convert_expression(node.slice)
|
398
|
-
|
399
|
-
# Create a function call to represent subscripting for now
|
400
|
-
# In the future, a dedicated SubscriptExpression node might be better
|
401
|
-
subscript_func = self.factory.create_identifier("__getitem__", loc)
|
402
|
-
return self.factory.create_function_call(
|
403
|
-
self.factory.create_attribute_access(value, "__getitem__", loc),
|
404
|
-
[index], [], loc
|
405
|
-
)
|
406
|
-
|
407
|
-
# Lambda expressions
|
408
|
-
elif isinstance(node, ast.Lambda):
|
409
|
-
loc = self._get_location(node)
|
410
|
-
# We don't have a dedicated lambda node, so warn and create a placeholder
|
411
|
-
self.error_handler.add_error(
|
412
|
-
ErrorType.PARSER_ERROR,
|
413
|
-
"Lambda expressions are not fully supported",
|
414
|
-
loc
|
415
|
-
)
|
416
|
-
|
417
|
-
# Create a placeholder function call
|
418
|
-
lambda_func = self.factory.create_identifier("lambda", loc)
|
419
|
-
return self.factory.create_function_call(lambda_func, [], [], loc)
|
420
|
-
|
421
|
-
# Dictionary literals
|
422
|
-
elif isinstance(node, ast.Dict):
|
423
|
-
loc = self._get_location(node)
|
424
|
-
# We don't have a dedicated dict node, so create a function call
|
425
|
-
dict_func = self.factory.create_identifier("dict", loc)
|
426
|
-
|
427
|
-
keywords = []
|
428
|
-
for i, (key, value) in enumerate(zip(node.keys, node.values)):
|
429
|
-
if key is None: # dict unpacking (**d)
|
430
|
-
self.error_handler.add_error(
|
431
|
-
ErrorType.PARSER_ERROR,
|
432
|
-
"Dictionary unpacking is not supported",
|
433
|
-
loc
|
434
|
-
)
|
435
|
-
continue
|
436
|
-
|
437
|
-
# For string keys, use them as keyword arguments
|
438
|
-
if isinstance(key, ast.Constant) and isinstance(key.value, str):
|
439
|
-
key_str = key.value
|
440
|
-
value_expr = self._convert_expression(value)
|
441
|
-
keywords.append(self.factory.create_keyword_argument(
|
442
|
-
key_str, value_expr, loc
|
443
|
-
))
|
444
|
-
else:
|
445
|
-
# For non-string keys, we need a different approach
|
446
|
-
self.error_handler.add_error(
|
447
|
-
ErrorType.PARSER_ERROR,
|
448
|
-
"Only string keys in dictionaries are fully supported",
|
449
|
-
loc
|
450
|
-
)
|
451
|
-
|
452
|
-
return self.factory.create_function_call(dict_func, [], keywords, loc)
|
453
|
-
|
454
|
-
# Fallback for unsupported nodes
|
455
|
-
self.error_handler.add_error(
|
456
|
-
ErrorType.PARSER_ERROR,
|
457
|
-
f"Unsupported expression type: {type(node).__name__}",
|
458
|
-
self._get_location(node)
|
459
|
-
)
|
460
|
-
# Return a simple identifier as fallback
|
461
|
-
return self.factory.create_identifier(
|
462
|
-
f"<unsupported:{type(node).__name__}>",
|
463
|
-
self._get_location(node)
|
464
|
-
)
|
465
|
-
|
466
|
-
def _convert_statement(self, node: ast.stmt) -> Statement:
|
467
|
-
"""Convert a Python statement node to a VEX AST statement."""
|
468
|
-
# Expression statements
|
469
|
-
if isinstance(node, ast.Expr):
|
470
|
-
expr = self._convert_expression(node.value)
|
471
|
-
return self.factory.create_expression_statement(
|
472
|
-
expr, self._get_location(node)
|
473
|
-
)
|
474
|
-
|
475
|
-
# Assignment statements
|
476
|
-
elif isinstance(node, ast.Assign):
|
477
|
-
# For simplicity, we'll only handle the first target
|
478
|
-
# (Python allows multiple targets like a = b = 1)
|
479
|
-
if not node.targets:
|
480
|
-
self.error_handler.add_error(
|
481
|
-
ErrorType.PARSER_ERROR,
|
482
|
-
"Assignment with no targets",
|
483
|
-
self._get_location(node)
|
484
|
-
)
|
485
|
-
# Fallback - create a dummy assignment
|
486
|
-
return self.factory.create_assignment(
|
487
|
-
self.factory.create_identifier("_dummy"),
|
488
|
-
self.factory.create_none_literal(),
|
489
|
-
self._get_location(node)
|
490
|
-
)
|
491
|
-
|
492
|
-
target = self._convert_expression(node.targets[0])
|
493
|
-
value = self._convert_expression(node.value)
|
494
|
-
return self.factory.create_assignment(
|
495
|
-
target, value, self._get_location(node)
|
496
|
-
)
|
497
|
-
|
498
|
-
# Augmented assignments (e.g., a += 1)
|
499
|
-
elif isinstance(node, ast.AugAssign):
|
500
|
-
loc = self._get_location(node)
|
501
|
-
target = self._convert_expression(node.target)
|
502
|
-
value = self._convert_expression(node.value)
|
503
|
-
|
504
|
-
# Map Python operator to VEX operator
|
505
|
-
op_type = type(node.op)
|
506
|
-
op_name = op_type.__name__
|
507
|
-
|
508
|
-
op_map = {
|
509
|
-
'Add': '+', 'Sub': '-', 'Mult': '*', 'Div': '/',
|
510
|
-
'FloorDiv': '//', 'Mod': '%', 'Pow': '**',
|
511
|
-
'LShift': '<<', 'RShift': '>>',
|
512
|
-
'BitOr': '|', 'BitXor': '^', 'BitAnd': '&',
|
513
|
-
'MatMult': '@'
|
514
|
-
}
|
515
|
-
|
516
|
-
if op_name in op_map:
|
517
|
-
op_str = op_map[op_name]
|
518
|
-
op = PYTHON_BINARY_OP_MAP.get(op_str)
|
519
|
-
|
520
|
-
if op:
|
521
|
-
# Create a binary operation (target op value)
|
522
|
-
bin_op = self.factory.create_binary_operation(
|
523
|
-
target, op, value, loc
|
524
|
-
)
|
525
|
-
|
526
|
-
# Create an assignment (target = bin_op)
|
527
|
-
return self.factory.create_assignment(
|
528
|
-
target, bin_op, loc
|
529
|
-
)
|
530
|
-
|
531
|
-
# Fallback for unknown operators
|
532
|
-
self.error_handler.add_error(
|
533
|
-
ErrorType.PARSER_ERROR,
|
534
|
-
f"Unsupported augmented assignment operator: {op_name}",
|
535
|
-
loc
|
536
|
-
)
|
537
|
-
# Create a basic assignment as fallback
|
538
|
-
return self.factory.create_assignment(target, value, loc)
|
539
|
-
|
540
|
-
# If statements
|
541
|
-
elif isinstance(node, ast.If):
|
542
|
-
test = self._convert_expression(node.test)
|
543
|
-
body = [self._convert_statement(stmt) for stmt in node.body]
|
544
|
-
loc = self._get_location(node)
|
545
|
-
|
546
|
-
# Handle else branch
|
547
|
-
orelse = None
|
548
|
-
if node.orelse:
|
549
|
-
# Check if it's an elif (a single If statement)
|
550
|
-
if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If):
|
551
|
-
orelse = self._convert_statement(node.orelse[0])
|
552
|
-
else:
|
553
|
-
# Regular else block
|
554
|
-
orelse = [self._convert_statement(stmt) for stmt in node.orelse]
|
555
|
-
|
556
|
-
return self.factory.create_if_statement(test, body, orelse, loc)
|
557
|
-
|
558
|
-
# While loops
|
559
|
-
elif isinstance(node, ast.While):
|
560
|
-
test = self._convert_expression(node.test)
|
561
|
-
body = [self._convert_statement(stmt) for stmt in node.body]
|
562
|
-
loc = self._get_location(node)
|
563
|
-
|
564
|
-
# Note: We're ignoring the else clause for now
|
565
|
-
if node.orelse:
|
566
|
-
self.error_handler.add_error(
|
567
|
-
ErrorType.PARSER_ERROR,
|
568
|
-
"While-else clauses are not supported",
|
569
|
-
loc
|
570
|
-
)
|
571
|
-
|
572
|
-
return self.factory.create_while_loop(test, body, loc)
|
573
|
-
|
574
|
-
# For loops
|
575
|
-
elif isinstance(node, ast.For):
|
576
|
-
target = self._convert_expression(node.target)
|
577
|
-
iter_expr = self._convert_expression(node.iter)
|
578
|
-
body = [self._convert_statement(stmt) for stmt in node.body]
|
579
|
-
loc = self._get_location(node)
|
580
|
-
|
581
|
-
# Note: We're ignoring the else clause for now
|
582
|
-
if node.orelse:
|
583
|
-
self.error_handler.add_error(
|
584
|
-
ErrorType.PARSER_ERROR,
|
585
|
-
"For-else clauses are not supported",
|
586
|
-
loc
|
587
|
-
)
|
588
|
-
|
589
|
-
return self.factory.create_for_loop(target, iter_expr, body, loc)
|
590
|
-
|
591
|
-
# Function definitions
|
592
|
-
elif isinstance(node, ast.FunctionDef):
|
593
|
-
loc = self._get_location(node)
|
594
|
-
|
595
|
-
# Convert arguments
|
596
|
-
args = []
|
597
|
-
for arg in node.args.args:
|
598
|
-
# Get annotation if present
|
599
|
-
annotation = None
|
600
|
-
if arg.annotation:
|
601
|
-
annotation = self._convert_expression(arg.annotation)
|
602
|
-
|
603
|
-
# Get default value if this argument has one
|
604
|
-
default = None
|
605
|
-
arg_idx = node.args.args.index(arg)
|
606
|
-
defaults_offset = len(node.args.args) - len(node.args.defaults)
|
607
|
-
if arg_idx >= defaults_offset and node.args.defaults:
|
608
|
-
default_idx = arg_idx - defaults_offset
|
609
|
-
if default_idx < len(node.args.defaults):
|
610
|
-
default_value = node.args.defaults[default_idx]
|
611
|
-
default = self._convert_expression(default_value)
|
612
|
-
|
613
|
-
args.append(Argument(arg.arg, annotation, default))
|
614
|
-
|
615
|
-
# Convert body
|
616
|
-
body = [self._convert_statement(stmt) for stmt in node.body]
|
617
|
-
|
618
|
-
# Convert return annotation if present
|
619
|
-
return_annotation = None
|
620
|
-
if node.returns:
|
621
|
-
return_annotation = self._convert_expression(node.returns)
|
622
|
-
|
623
|
-
return self.factory.create_function_definition(
|
624
|
-
node.name, args, body, return_annotation, loc
|
625
|
-
)
|
626
|
-
|
627
|
-
# Return statements
|
628
|
-
elif isinstance(node, ast.Return):
|
629
|
-
value = None
|
630
|
-
if node.value:
|
631
|
-
value = self._convert_expression(node.value)
|
632
|
-
return self.factory.create_return_statement(
|
633
|
-
value, self._get_location(node)
|
634
|
-
)
|
635
|
-
|
636
|
-
# Break statements
|
637
|
-
elif isinstance(node, ast.Break):
|
638
|
-
return self.factory.create_break_statement(
|
639
|
-
self._get_location(node)
|
640
|
-
)
|
641
|
-
|
642
|
-
# Continue statements
|
643
|
-
elif isinstance(node, ast.Continue):
|
644
|
-
return self.factory.create_continue_statement(
|
645
|
-
self._get_location(node)
|
646
|
-
)
|
647
|
-
|
648
|
-
# Pass statements - convert to empty expression statement
|
649
|
-
elif isinstance(node, ast.Pass):
|
650
|
-
return self.factory.create_expression_statement(
|
651
|
-
self.factory.create_none_literal(),
|
652
|
-
self._get_location(node)
|
653
|
-
)
|
654
|
-
|
655
|
-
# Import statements
|
656
|
-
elif isinstance(node, ast.Import):
|
657
|
-
loc = self._get_location(node)
|
658
|
-
# Create a list of assignments for each imported name
|
659
|
-
statements = []
|
660
|
-
|
661
|
-
for name in node.names:
|
662
|
-
# Create an identifier for the module
|
663
|
-
module_name = name.name
|
664
|
-
as_name = name.asname or module_name
|
665
|
-
|
666
|
-
# Create an assignment: as_name = module_name
|
667
|
-
target = self.factory.create_identifier(as_name, loc)
|
668
|
-
value = self.factory.create_identifier(f"<import:{module_name}>", loc)
|
669
|
-
|
670
|
-
statements.append(self.factory.create_assignment(target, value, loc))
|
671
|
-
|
672
|
-
# If there's only one statement, return it
|
673
|
-
if len(statements) == 1:
|
674
|
-
return statements[0]
|
675
|
-
|
676
|
-
# Otherwise, return the first one and add a warning
|
677
|
-
if len(statements) > 1:
|
678
|
-
self.error_handler.add_error(
|
679
|
-
ErrorType.PARSER_ERROR,
|
680
|
-
"Multiple imports in a single statement are not fully supported",
|
681
|
-
loc
|
682
|
-
)
|
683
|
-
|
684
|
-
return statements[0]
|
685
|
-
|
686
|
-
# Import from statements
|
687
|
-
elif isinstance(node, ast.ImportFrom):
|
688
|
-
loc = self._get_location(node)
|
689
|
-
module_name = node.module or ""
|
690
|
-
|
691
|
-
# Special case for "from vex import *"
|
692
|
-
if module_name == "vex" and any(name.name == "*" for name in node.names):
|
693
|
-
# Create a special identifier that represents "from vex import *"
|
694
|
-
return self.factory.create_expression_statement(
|
695
|
-
self.factory.create_identifier("<import:vex:*>", loc),
|
696
|
-
loc
|
697
|
-
)
|
698
|
-
|
699
|
-
# For other import from statements, create assignments
|
700
|
-
statements = []
|
701
|
-
|
702
|
-
for name in node.names:
|
703
|
-
# Create an identifier for the imported name
|
704
|
-
imported_name = name.name
|
705
|
-
as_name = name.asname or imported_name
|
706
|
-
|
707
|
-
# Create an assignment: as_name = module_name.imported_name
|
708
|
-
target = self.factory.create_identifier(as_name, loc)
|
709
|
-
value = self.factory.create_identifier(f"<import:{module_name}.{imported_name}>", loc)
|
710
|
-
|
711
|
-
statements.append(self.factory.create_assignment(target, value, loc))
|
712
|
-
|
713
|
-
# If there's only one statement, return it
|
714
|
-
if len(statements) == 1:
|
715
|
-
return statements[0]
|
716
|
-
|
717
|
-
# Otherwise, return the first one and add a warning
|
718
|
-
if len(statements) > 1:
|
719
|
-
self.error_handler.add_error(
|
720
|
-
ErrorType.PARSER_ERROR,
|
721
|
-
"Multiple imports in a single statement are not fully supported",
|
722
|
-
loc
|
723
|
-
)
|
724
|
-
|
725
|
-
return statements[0]
|
726
|
-
|
727
|
-
# Class definitions - not supported yet
|
728
|
-
elif isinstance(node, ast.ClassDef):
|
729
|
-
loc = self._get_location(node)
|
730
|
-
self.error_handler.add_error(
|
731
|
-
ErrorType.PARSER_ERROR,
|
732
|
-
"Class definitions are not supported",
|
733
|
-
loc
|
734
|
-
)
|
735
|
-
# Create a placeholder expression statement
|
736
|
-
return self.factory.create_expression_statement(
|
737
|
-
self.factory.create_identifier(
|
738
|
-
f"<class:{node.name}>",
|
739
|
-
loc
|
740
|
-
),
|
741
|
-
loc
|
742
|
-
)
|
743
|
-
|
744
|
-
# Fallback for unsupported nodes
|
745
|
-
self.error_handler.add_error(
|
746
|
-
ErrorType.PARSER_ERROR,
|
747
|
-
f"Unsupported statement type: {type(node).__name__}",
|
748
|
-
self._get_location(node)
|
749
|
-
)
|
750
|
-
# Return a simple expression statement as fallback
|
751
|
-
return self.factory.create_expression_statement(
|
752
|
-
self.factory.create_identifier(
|
753
|
-
f"<unsupported:{type(node).__name__}>",
|
754
|
-
self._get_location(node)
|
755
|
-
),
|
756
|
-
self._get_location(node)
|
757
|
-
)
|
758
|
-
|
759
|
-
def parse(self) -> Program:
|
760
|
-
"""Parse the Python source code and return a VEX AST."""
|
761
|
-
try:
|
762
|
-
# Dedent the source code to remove whitespace
|
763
|
-
dedented_source = textwrap.dedent(self.source)
|
764
|
-
|
765
|
-
# Parse the Python code with modern features
|
766
|
-
self._py_ast = ast.parse(
|
767
|
-
dedented_source,
|
768
|
-
filename=self.filename,
|
769
|
-
feature_version=(3, 8) # Explicitly use Python 3.8+ features
|
770
|
-
)
|
771
|
-
|
772
|
-
# Convert the module body to VEX statements
|
773
|
-
body = [self._convert_statement(stmt) for stmt in self._py_ast.body]
|
774
|
-
|
775
|
-
# Create and return the program node
|
776
|
-
return self.factory.create_program(body)
|
777
|
-
|
778
|
-
except SyntaxError as e:
|
779
|
-
# Convert Python SyntaxError to VexSyntaxError
|
780
|
-
loc = SourceLocation(
|
781
|
-
line=e.lineno or 1,
|
782
|
-
column=e.offset or 1,
|
783
|
-
filename=e.filename or self.filename
|
784
|
-
)
|
785
|
-
if hasattr(e, 'end_lineno') and e.end_lineno is not None and \
|
786
|
-
hasattr(e, 'end_offset') and e.end_offset is not None:
|
787
|
-
loc.end_line = e.end_lineno
|
788
|
-
loc.end_column = e.end_offset
|
789
|
-
|
790
|
-
self.error_handler.add_error(
|
791
|
-
ErrorType.PARSER_ERROR,
|
792
|
-
f"Syntax error: {e.msg}",
|
793
|
-
loc
|
794
|
-
)
|
795
|
-
|
796
|
-
# Only raise if the error handler is configured to do so
|
797
|
-
if self.error_handler._raise_on_error:
|
798
|
-
raise VexSyntaxError(f"Syntax error: {e.msg}", loc) from e
|
799
|
-
|
800
|
-
# Return an empty program if we're not raising
|
801
|
-
return self.factory.create_program([])
|
802
|
-
|
803
|
-
except Exception as e:
|
804
|
-
# Handle other parsing errors
|
805
|
-
self.error_handler.add_error(
|
806
|
-
ErrorType.PARSER_ERROR,
|
807
|
-
f"Failed to parse Python code: {str(e)}",
|
808
|
-
SourceLocation(1, 1, self.filename)
|
809
|
-
)
|
810
|
-
raise VexSyntaxError(
|
811
|
-
f"Failed to parse Python code: {str(e)}",
|
812
|
-
SourceLocation(1, 1, self.filename)
|
813
|
-
) from e
|
814
|
-
|
815
|
-
# Convenience functions
|
816
|
-
def parse_string(source: str, filename: str = "<string>",
|
817
|
-
error_handler: Optional[ErrorHandler] = None) -> Program:
|
818
|
-
"""Parse Python code from a string."""
|
819
|
-
parser = PythonParser(source, filename, error_handler)
|
820
|
-
return parser.parse()
|
821
|
-
|
822
|
-
def parse_file(filepath: str, error_handler: Optional[ErrorHandler] = None) -> Program:
|
823
|
-
"""Parse Python code from a file."""
|
824
|
-
try:
|
825
|
-
with open(filepath, 'r', encoding='utf-8') as f:
|
826
|
-
source = f.read()
|
827
|
-
return parse_string(source, filepath, error_handler)
|
828
|
-
except FileNotFoundError:
|
829
|
-
raise
|
830
|
-
except IOError as e:
|
831
|
-
raise IOError(f"Error reading file {filepath}: {e}")
|
1
|
+
"""Python code parser implementation."""
|
2
|
+
|
3
|
+
import ast
|
4
|
+
import textwrap
|
5
|
+
from typing import Any, Dict, List, Optional, Type, Union, cast
|
6
|
+
|
7
|
+
from .interfaces import BaseParser
|
8
|
+
from .factory import NodeFactory
|
9
|
+
|
10
|
+
from ..ast.core import Expression, Program, Statement
|
11
|
+
from ..ast.expressions import (
|
12
|
+
AttributeAccess, BinaryOperation, FunctionCall, Identifier, KeywordArgument,
|
13
|
+
UnaryOperation, VariableReference
|
14
|
+
)
|
15
|
+
|
16
|
+
from ..ast.interfaces import IExpression, IStatement
|
17
|
+
from ..ast.literals import (
|
18
|
+
BooleanLiteral, NoneLiteral, NumberLiteral, StringLiteral
|
19
|
+
)
|
20
|
+
|
21
|
+
from ..ast.operators import Operator, PYTHON_BINARY_OP_MAP, PYTHON_UNARY_OP_MAP, PYTHON_COMP_OP_MAP
|
22
|
+
from ..ast.statements import (
|
23
|
+
Argument, Assignment, BreakStatement, ContinueStatement, ExpressionStatement,
|
24
|
+
ForLoop, FunctionDefinition, IfStatement, ReturnStatement, WhileLoop
|
25
|
+
)
|
26
|
+
from ..ast.vex_nodes import create_vex_api_call, VexAPICall
|
27
|
+
from ..registry.registry import registry
|
28
|
+
|
29
|
+
from ..utils.errors import ErrorHandler, ErrorType, VexSyntaxError
|
30
|
+
from ..utils.source_location import SourceLocation
|
31
|
+
|
32
|
+
class PythonParser(BaseParser):
|
33
|
+
"""Parser for Python code using the built-in ast module (Python 3.8+)."""
|
34
|
+
|
35
|
+
def __init__(self, source: str, filename: str = "<string>",
|
36
|
+
error_handler: Optional[ErrorHandler] = None):
|
37
|
+
super().__init__(error_handler)
|
38
|
+
self.source = source
|
39
|
+
self.filename = filename
|
40
|
+
self.factory = NodeFactory(error_handler)
|
41
|
+
self._py_ast: Optional[ast.Module] = None
|
42
|
+
|
43
|
+
def _get_location(self, node: ast.AST) -> Optional[SourceLocation]:
|
44
|
+
"""Extract source location from a Python AST node."""
|
45
|
+
if hasattr(node, 'lineno'):
|
46
|
+
loc = SourceLocation(
|
47
|
+
line=node.lineno,
|
48
|
+
column=node.col_offset + 1, # ast is 0-indexed
|
49
|
+
filename=self.filename
|
50
|
+
)
|
51
|
+
|
52
|
+
# Add end position if available (Python 3.8+)
|
53
|
+
if hasattr(node, 'end_lineno') and node.end_lineno is not None and \
|
54
|
+
hasattr(node, 'end_col_offset') and node.end_col_offset is not None:
|
55
|
+
loc.end_line = node.end_lineno
|
56
|
+
loc.end_column = node.end_col_offset + 1
|
57
|
+
|
58
|
+
return loc
|
59
|
+
return None
|
60
|
+
|
61
|
+
def _convert_expression(self, node: ast.expr) -> Expression:
|
62
|
+
"""Convert a Python expression node to a VEX AST expression."""
|
63
|
+
# Handle literal values - using modern Constant node (Python 3.8+)
|
64
|
+
if isinstance(node, ast.Constant):
|
65
|
+
value = node.value
|
66
|
+
loc = self._get_location(node)
|
67
|
+
|
68
|
+
if isinstance(value, (int, float)):
|
69
|
+
return self.factory.create_number_literal(value, loc)
|
70
|
+
elif isinstance(value, str):
|
71
|
+
return self.factory.create_string_literal(value, loc)
|
72
|
+
elif isinstance(value, bool):
|
73
|
+
return self.factory.create_boolean_literal(value, loc)
|
74
|
+
elif value is None:
|
75
|
+
return self.factory.create_none_literal(loc)
|
76
|
+
else:
|
77
|
+
self.error_handler.add_error(
|
78
|
+
ErrorType.PARSER_ERROR,
|
79
|
+
f"Unsupported constant type: {type(value).__name__}",
|
80
|
+
loc
|
81
|
+
)
|
82
|
+
# Fallback - treat as string
|
83
|
+
return self.factory.create_string_literal(str(value), loc)
|
84
|
+
|
85
|
+
# Variables
|
86
|
+
elif isinstance(node, ast.Name):
|
87
|
+
loc = self._get_location(node)
|
88
|
+
ident = self.factory.create_identifier(node.id, loc)
|
89
|
+
# In a load context, create a variable reference
|
90
|
+
if isinstance(node.ctx, ast.Load):
|
91
|
+
return self.factory.create_variable_reference(ident, loc)
|
92
|
+
# For store and del contexts, just return the identifier
|
93
|
+
# These will be handled by parent nodes (e.g., Assignment)
|
94
|
+
return ident
|
95
|
+
|
96
|
+
# Attribute access (e.g., left_motor.set_velocity)
|
97
|
+
elif isinstance(node, ast.Attribute):
|
98
|
+
value = self._convert_expression(node.value)
|
99
|
+
loc = self._get_location(node)
|
100
|
+
|
101
|
+
# Create a proper AttributeAccess node
|
102
|
+
return self.factory.create_attribute_access(value, node.attr, loc)
|
103
|
+
|
104
|
+
# Binary operations
|
105
|
+
elif isinstance(node, ast.BinOp):
|
106
|
+
left = self._convert_expression(node.left)
|
107
|
+
right = self._convert_expression(node.right)
|
108
|
+
loc = self._get_location(node)
|
109
|
+
|
110
|
+
# Map Python operator to VEX operator
|
111
|
+
op_type = type(node.op)
|
112
|
+
op_name = op_type.__name__
|
113
|
+
|
114
|
+
op_map = {
|
115
|
+
'Add': '+', 'Sub': '-', 'Mult': '*', 'Div': '/',
|
116
|
+
'FloorDiv': '//', 'Mod': '%', 'Pow': '**',
|
117
|
+
'LShift': '<<', 'RShift': '>>',
|
118
|
+
'BitOr': '|', 'BitXor': '^', 'BitAnd': '&',
|
119
|
+
'MatMult': '@'
|
120
|
+
}
|
121
|
+
|
122
|
+
if op_name in op_map:
|
123
|
+
op_str = op_map[op_name]
|
124
|
+
op = PYTHON_BINARY_OP_MAP.get(op_str)
|
125
|
+
if op:
|
126
|
+
return self.factory.create_binary_operation(left, op, right, loc)
|
127
|
+
|
128
|
+
# Fallback for unknown operators
|
129
|
+
self.error_handler.add_error(
|
130
|
+
ErrorType.PARSER_ERROR,
|
131
|
+
f"Unsupported binary operator: {op_name}",
|
132
|
+
loc
|
133
|
+
)
|
134
|
+
# Create a basic operation with the operator as a string
|
135
|
+
return self.factory.create_binary_operation(
|
136
|
+
left, Operator.ADD, right, loc
|
137
|
+
)
|
138
|
+
|
139
|
+
# Unary operations
|
140
|
+
elif isinstance(node, ast.UnaryOp):
|
141
|
+
operand = self._convert_expression(node.operand)
|
142
|
+
loc = self._get_location(node)
|
143
|
+
|
144
|
+
# Map Python unary operator to VEX operator
|
145
|
+
op_type = type(node.op)
|
146
|
+
op_name = op_type.__name__
|
147
|
+
|
148
|
+
op_map = {
|
149
|
+
'UAdd': '+', 'USub': '-', 'Not': 'not', 'Invert': '~'
|
150
|
+
}
|
151
|
+
|
152
|
+
if op_name in op_map:
|
153
|
+
op_str = op_map[op_name]
|
154
|
+
op = PYTHON_UNARY_OP_MAP.get(op_str)
|
155
|
+
if op:
|
156
|
+
return self.factory.create_unary_operation(op, operand, loc)
|
157
|
+
|
158
|
+
# Fallback for unknown operators
|
159
|
+
self.error_handler.add_error(
|
160
|
+
ErrorType.PARSER_ERROR,
|
161
|
+
f"Unsupported unary operator: {op_name}",
|
162
|
+
loc
|
163
|
+
)
|
164
|
+
# Create a basic operation with a default operator
|
165
|
+
return self.factory.create_unary_operation(
|
166
|
+
Operator.UNARY_PLUS, operand, loc
|
167
|
+
)
|
168
|
+
|
169
|
+
# Function calls
|
170
|
+
elif isinstance(node, ast.Call):
|
171
|
+
func = self._convert_expression(node.func)
|
172
|
+
args = [self._convert_expression(arg) for arg in node.args]
|
173
|
+
keywords = []
|
174
|
+
loc = self._get_location(node)
|
175
|
+
|
176
|
+
for kw in node.keywords:
|
177
|
+
if kw.arg is None: # **kwargs
|
178
|
+
self.error_handler.add_error(
|
179
|
+
ErrorType.PARSER_ERROR,
|
180
|
+
"Keyword argument unpacking (**kwargs) is not supported",
|
181
|
+
self._get_location(kw)
|
182
|
+
)
|
183
|
+
continue
|
184
|
+
|
185
|
+
value = self._convert_expression(kw.value)
|
186
|
+
keyword = self.factory.create_keyword_argument(
|
187
|
+
kw.arg, value, self._get_location(kw)
|
188
|
+
)
|
189
|
+
keywords.append(keyword)
|
190
|
+
|
191
|
+
# Check if this is a VEX API call
|
192
|
+
function_name = None
|
193
|
+
if hasattr(func, 'name'):
|
194
|
+
function_name = func.name
|
195
|
+
elif hasattr(func, 'attribute') and hasattr(func, 'object'):
|
196
|
+
obj = func.object
|
197
|
+
attr = func.attribute
|
198
|
+
if hasattr(obj, 'name'):
|
199
|
+
function_name = f"{obj.name}.{attr}"
|
200
|
+
|
201
|
+
# For debugging
|
202
|
+
# print(f"Function call: {function_name}")
|
203
|
+
# print(f"Registry has function: {registry.get_function(function_name) is not None}")
|
204
|
+
|
205
|
+
# Check for common VEX API patterns
|
206
|
+
is_vex_api_call = False
|
207
|
+
|
208
|
+
if function_name:
|
209
|
+
# Check if this is a method call on a known object type
|
210
|
+
if '.' in function_name:
|
211
|
+
obj_name, method_name = function_name.split('.', 1)
|
212
|
+
|
213
|
+
# Common VEX method names
|
214
|
+
vex_methods = ['spin', 'stop', 'set_velocity', 'spin_for', 'spin_to_position',
|
215
|
+
'print', 'clear', 'set_font', 'set_pen', 'draw_line', 'draw_rectangle',
|
216
|
+
'rotation', 'heading', 'temperature', 'pressing', 'position']
|
217
|
+
|
218
|
+
# Common VEX object names
|
219
|
+
vex_objects = ['motor', 'brain', 'controller', 'drivetrain', 'gyro', 'vision',
|
220
|
+
'distance', 'inertial', 'optical', 'gps', 'bumper', 'limit']
|
221
|
+
|
222
|
+
# Check if method name is a known VEX method
|
223
|
+
if method_name in vex_methods:
|
224
|
+
is_vex_api_call = True
|
225
|
+
|
226
|
+
# Check if object name starts with a known VEX object type
|
227
|
+
for vex_obj in vex_objects:
|
228
|
+
if obj_name.startswith(vex_obj):
|
229
|
+
is_vex_api_call = True
|
230
|
+
break
|
231
|
+
|
232
|
+
# Check registry
|
233
|
+
if registry.get_function(method_name):
|
234
|
+
is_vex_api_call = True
|
235
|
+
|
236
|
+
# Or check if it's a direct function
|
237
|
+
else:
|
238
|
+
# Common VEX function names
|
239
|
+
vex_functions = ['wait', 'wait_until', 'sleep', 'rumble']
|
240
|
+
|
241
|
+
# Special case for 'print': never treat as VEX API call in test files
|
242
|
+
if function_name == 'print':
|
243
|
+
# Check if this is a test file
|
244
|
+
is_test_file = 'test_' in self.filename
|
245
|
+
# Always treat 'print' as a regular function call in test files
|
246
|
+
if not is_test_file:
|
247
|
+
is_vex_api_call = True
|
248
|
+
else:
|
249
|
+
# Explicitly set to False to ensure it's never treated as a VEX API call in test files
|
250
|
+
is_vex_api_call = False
|
251
|
+
elif function_name in vex_functions:
|
252
|
+
is_vex_api_call = True
|
253
|
+
|
254
|
+
# Check registry, but don't override 'print' in test files
|
255
|
+
if registry.get_function(function_name):
|
256
|
+
# Only set to True if we're not dealing with 'print' in a test file
|
257
|
+
if not (function_name == 'print' and 'test_' in self.filename):
|
258
|
+
is_vex_api_call = True
|
259
|
+
|
260
|
+
if is_vex_api_call:
|
261
|
+
return create_vex_api_call(func, args, keywords, loc)
|
262
|
+
|
263
|
+
# Regular function call
|
264
|
+
return self.factory.create_function_call(func, args, keywords, loc)
|
265
|
+
|
266
|
+
|
267
|
+
# Comparison operations (e.g., a < b, x == y)
|
268
|
+
elif isinstance(node, ast.Compare):
|
269
|
+
# Handle the first comparison
|
270
|
+
left = self._convert_expression(node.left)
|
271
|
+
loc = self._get_location(node)
|
272
|
+
|
273
|
+
if not node.ops or not node.comparators:
|
274
|
+
self.error_handler.add_error(
|
275
|
+
ErrorType.PARSER_ERROR,
|
276
|
+
"Invalid comparison with no operators or comparators",
|
277
|
+
loc
|
278
|
+
)
|
279
|
+
# Return a placeholder expression
|
280
|
+
return left
|
281
|
+
|
282
|
+
# Process each comparison operator and right operand
|
283
|
+
result = left
|
284
|
+
for i, (op, comparator) in enumerate(zip(node.ops, node.comparators)):
|
285
|
+
right = self._convert_expression(comparator)
|
286
|
+
|
287
|
+
# Map Python comparison operator to VEX operator
|
288
|
+
op_type = type(op)
|
289
|
+
op_name = op_type.__name__
|
290
|
+
|
291
|
+
op_map = {
|
292
|
+
'Eq': '==', 'NotEq': '!=',
|
293
|
+
'Lt': '<', 'LtE': '<=',
|
294
|
+
'Gt': '>', 'GtE': '>=',
|
295
|
+
'Is': 'is', 'IsNot': 'is not',
|
296
|
+
'In': 'in', 'NotIn': 'not in'
|
297
|
+
}
|
298
|
+
|
299
|
+
if op_name in op_map:
|
300
|
+
op_str = op_map[op_name]
|
301
|
+
vex_op = PYTHON_COMP_OP_MAP.get(op_str)
|
302
|
+
|
303
|
+
if vex_op:
|
304
|
+
# For the first comparison, use left and right
|
305
|
+
# For subsequent comparisons, use previous result and right
|
306
|
+
result = self.factory.create_binary_operation(
|
307
|
+
result, vex_op, right, loc
|
308
|
+
)
|
309
|
+
else:
|
310
|
+
self.error_handler.add_error(
|
311
|
+
ErrorType.PARSER_ERROR,
|
312
|
+
f"Unsupported comparison operator: {op_name}",
|
313
|
+
loc
|
314
|
+
)
|
315
|
+
else:
|
316
|
+
self.error_handler.add_error(
|
317
|
+
ErrorType.PARSER_ERROR,
|
318
|
+
f"Unknown comparison operator: {op_name}",
|
319
|
+
loc
|
320
|
+
)
|
321
|
+
|
322
|
+
return result
|
323
|
+
|
324
|
+
# Boolean operations (and, or)
|
325
|
+
elif isinstance(node, ast.BoolOp):
|
326
|
+
loc = self._get_location(node)
|
327
|
+
|
328
|
+
if not node.values:
|
329
|
+
self.error_handler.add_error(
|
330
|
+
ErrorType.PARSER_ERROR,
|
331
|
+
"Boolean operation with no values",
|
332
|
+
loc
|
333
|
+
)
|
334
|
+
# Return a placeholder expression
|
335
|
+
return self.factory.create_boolean_literal(False, loc)
|
336
|
+
|
337
|
+
# Get the operator
|
338
|
+
op_type = type(node.op)
|
339
|
+
op_name = op_type.__name__
|
340
|
+
|
341
|
+
op_map = {
|
342
|
+
'And': Operator.LOGICAL_AND,
|
343
|
+
'Or': Operator.LOGICAL_OR
|
344
|
+
}
|
345
|
+
|
346
|
+
if op_name in op_map:
|
347
|
+
vex_op = op_map[op_name]
|
348
|
+
else:
|
349
|
+
self.error_handler.add_error(
|
350
|
+
ErrorType.PARSER_ERROR,
|
351
|
+
f"Unknown boolean operator: {op_name}",
|
352
|
+
loc
|
353
|
+
)
|
354
|
+
vex_op = Operator.LOGICAL_AND # Fallback
|
355
|
+
|
356
|
+
# Process all values from left to right
|
357
|
+
values = [self._convert_expression(val) for val in node.values]
|
358
|
+
|
359
|
+
# Build the expression tree from left to right
|
360
|
+
result = values[0]
|
361
|
+
for right in values[1:]:
|
362
|
+
result = self.factory.create_binary_operation(
|
363
|
+
result, vex_op, right, loc
|
364
|
+
)
|
365
|
+
|
366
|
+
return result
|
367
|
+
|
368
|
+
# Conditional expressions (ternary operators)
|
369
|
+
elif isinstance(node, ast.IfExp):
|
370
|
+
loc = self._get_location(node)
|
371
|
+
test = self._convert_expression(node.test)
|
372
|
+
body = self._convert_expression(node.body)
|
373
|
+
orelse = self._convert_expression(node.orelse)
|
374
|
+
|
375
|
+
return self.factory.create_conditional_expression(test, body, orelse, loc)
|
376
|
+
|
377
|
+
# List literals
|
378
|
+
elif isinstance(node, ast.List) or isinstance(node, ast.Tuple):
|
379
|
+
# We don't have a dedicated list/tuple node, so use function call
|
380
|
+
# with a special identifier for now
|
381
|
+
loc = self._get_location(node)
|
382
|
+
elements = [self._convert_expression(elt) for elt in node.elts]
|
383
|
+
list_name = "list" if isinstance(node, ast.List) else "tuple"
|
384
|
+
list_func = self.factory.create_identifier(list_name, loc)
|
385
|
+
|
386
|
+
return self.factory.create_function_call(list_func, elements, [], loc)
|
387
|
+
|
388
|
+
# Subscript (indexing) expressions like a[b]
|
389
|
+
elif isinstance(node, ast.Subscript):
|
390
|
+
loc = self._get_location(node)
|
391
|
+
value = self._convert_expression(node.value)
|
392
|
+
|
393
|
+
# Convert the slice/index
|
394
|
+
if isinstance(node.slice, ast.Index): # Python < 3.9
|
395
|
+
index = self._convert_expression(node.slice.value)
|
396
|
+
else: # Python 3.9+
|
397
|
+
index = self._convert_expression(node.slice)
|
398
|
+
|
399
|
+
# Create a function call to represent subscripting for now
|
400
|
+
# In the future, a dedicated SubscriptExpression node might be better
|
401
|
+
subscript_func = self.factory.create_identifier("__getitem__", loc)
|
402
|
+
return self.factory.create_function_call(
|
403
|
+
self.factory.create_attribute_access(value, "__getitem__", loc),
|
404
|
+
[index], [], loc
|
405
|
+
)
|
406
|
+
|
407
|
+
# Lambda expressions
|
408
|
+
elif isinstance(node, ast.Lambda):
|
409
|
+
loc = self._get_location(node)
|
410
|
+
# We don't have a dedicated lambda node, so warn and create a placeholder
|
411
|
+
self.error_handler.add_error(
|
412
|
+
ErrorType.PARSER_ERROR,
|
413
|
+
"Lambda expressions are not fully supported",
|
414
|
+
loc
|
415
|
+
)
|
416
|
+
|
417
|
+
# Create a placeholder function call
|
418
|
+
lambda_func = self.factory.create_identifier("lambda", loc)
|
419
|
+
return self.factory.create_function_call(lambda_func, [], [], loc)
|
420
|
+
|
421
|
+
# Dictionary literals
|
422
|
+
elif isinstance(node, ast.Dict):
|
423
|
+
loc = self._get_location(node)
|
424
|
+
# We don't have a dedicated dict node, so create a function call
|
425
|
+
dict_func = self.factory.create_identifier("dict", loc)
|
426
|
+
|
427
|
+
keywords = []
|
428
|
+
for i, (key, value) in enumerate(zip(node.keys, node.values)):
|
429
|
+
if key is None: # dict unpacking (**d)
|
430
|
+
self.error_handler.add_error(
|
431
|
+
ErrorType.PARSER_ERROR,
|
432
|
+
"Dictionary unpacking is not supported",
|
433
|
+
loc
|
434
|
+
)
|
435
|
+
continue
|
436
|
+
|
437
|
+
# For string keys, use them as keyword arguments
|
438
|
+
if isinstance(key, ast.Constant) and isinstance(key.value, str):
|
439
|
+
key_str = key.value
|
440
|
+
value_expr = self._convert_expression(value)
|
441
|
+
keywords.append(self.factory.create_keyword_argument(
|
442
|
+
key_str, value_expr, loc
|
443
|
+
))
|
444
|
+
else:
|
445
|
+
# For non-string keys, we need a different approach
|
446
|
+
self.error_handler.add_error(
|
447
|
+
ErrorType.PARSER_ERROR,
|
448
|
+
"Only string keys in dictionaries are fully supported",
|
449
|
+
loc
|
450
|
+
)
|
451
|
+
|
452
|
+
return self.factory.create_function_call(dict_func, [], keywords, loc)
|
453
|
+
|
454
|
+
# Fallback for unsupported nodes
|
455
|
+
self.error_handler.add_error(
|
456
|
+
ErrorType.PARSER_ERROR,
|
457
|
+
f"Unsupported expression type: {type(node).__name__}",
|
458
|
+
self._get_location(node)
|
459
|
+
)
|
460
|
+
# Return a simple identifier as fallback
|
461
|
+
return self.factory.create_identifier(
|
462
|
+
f"<unsupported:{type(node).__name__}>",
|
463
|
+
self._get_location(node)
|
464
|
+
)
|
465
|
+
|
466
|
+
def _convert_statement(self, node: ast.stmt) -> Statement:
|
467
|
+
"""Convert a Python statement node to a VEX AST statement."""
|
468
|
+
# Expression statements
|
469
|
+
if isinstance(node, ast.Expr):
|
470
|
+
expr = self._convert_expression(node.value)
|
471
|
+
return self.factory.create_expression_statement(
|
472
|
+
expr, self._get_location(node)
|
473
|
+
)
|
474
|
+
|
475
|
+
# Assignment statements
|
476
|
+
elif isinstance(node, ast.Assign):
|
477
|
+
# For simplicity, we'll only handle the first target
|
478
|
+
# (Python allows multiple targets like a = b = 1)
|
479
|
+
if not node.targets:
|
480
|
+
self.error_handler.add_error(
|
481
|
+
ErrorType.PARSER_ERROR,
|
482
|
+
"Assignment with no targets",
|
483
|
+
self._get_location(node)
|
484
|
+
)
|
485
|
+
# Fallback - create a dummy assignment
|
486
|
+
return self.factory.create_assignment(
|
487
|
+
self.factory.create_identifier("_dummy"),
|
488
|
+
self.factory.create_none_literal(),
|
489
|
+
self._get_location(node)
|
490
|
+
)
|
491
|
+
|
492
|
+
target = self._convert_expression(node.targets[0])
|
493
|
+
value = self._convert_expression(node.value)
|
494
|
+
return self.factory.create_assignment(
|
495
|
+
target, value, self._get_location(node)
|
496
|
+
)
|
497
|
+
|
498
|
+
# Augmented assignments (e.g., a += 1)
|
499
|
+
elif isinstance(node, ast.AugAssign):
|
500
|
+
loc = self._get_location(node)
|
501
|
+
target = self._convert_expression(node.target)
|
502
|
+
value = self._convert_expression(node.value)
|
503
|
+
|
504
|
+
# Map Python operator to VEX operator
|
505
|
+
op_type = type(node.op)
|
506
|
+
op_name = op_type.__name__
|
507
|
+
|
508
|
+
op_map = {
|
509
|
+
'Add': '+', 'Sub': '-', 'Mult': '*', 'Div': '/',
|
510
|
+
'FloorDiv': '//', 'Mod': '%', 'Pow': '**',
|
511
|
+
'LShift': '<<', 'RShift': '>>',
|
512
|
+
'BitOr': '|', 'BitXor': '^', 'BitAnd': '&',
|
513
|
+
'MatMult': '@'
|
514
|
+
}
|
515
|
+
|
516
|
+
if op_name in op_map:
|
517
|
+
op_str = op_map[op_name]
|
518
|
+
op = PYTHON_BINARY_OP_MAP.get(op_str)
|
519
|
+
|
520
|
+
if op:
|
521
|
+
# Create a binary operation (target op value)
|
522
|
+
bin_op = self.factory.create_binary_operation(
|
523
|
+
target, op, value, loc
|
524
|
+
)
|
525
|
+
|
526
|
+
# Create an assignment (target = bin_op)
|
527
|
+
return self.factory.create_assignment(
|
528
|
+
target, bin_op, loc
|
529
|
+
)
|
530
|
+
|
531
|
+
# Fallback for unknown operators
|
532
|
+
self.error_handler.add_error(
|
533
|
+
ErrorType.PARSER_ERROR,
|
534
|
+
f"Unsupported augmented assignment operator: {op_name}",
|
535
|
+
loc
|
536
|
+
)
|
537
|
+
# Create a basic assignment as fallback
|
538
|
+
return self.factory.create_assignment(target, value, loc)
|
539
|
+
|
540
|
+
# If statements
|
541
|
+
elif isinstance(node, ast.If):
|
542
|
+
test = self._convert_expression(node.test)
|
543
|
+
body = [self._convert_statement(stmt) for stmt in node.body]
|
544
|
+
loc = self._get_location(node)
|
545
|
+
|
546
|
+
# Handle else branch
|
547
|
+
orelse = None
|
548
|
+
if node.orelse:
|
549
|
+
# Check if it's an elif (a single If statement)
|
550
|
+
if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If):
|
551
|
+
orelse = self._convert_statement(node.orelse[0])
|
552
|
+
else:
|
553
|
+
# Regular else block
|
554
|
+
orelse = [self._convert_statement(stmt) for stmt in node.orelse]
|
555
|
+
|
556
|
+
return self.factory.create_if_statement(test, body, orelse, loc)
|
557
|
+
|
558
|
+
# While loops
|
559
|
+
elif isinstance(node, ast.While):
|
560
|
+
test = self._convert_expression(node.test)
|
561
|
+
body = [self._convert_statement(stmt) for stmt in node.body]
|
562
|
+
loc = self._get_location(node)
|
563
|
+
|
564
|
+
# Note: We're ignoring the else clause for now
|
565
|
+
if node.orelse:
|
566
|
+
self.error_handler.add_error(
|
567
|
+
ErrorType.PARSER_ERROR,
|
568
|
+
"While-else clauses are not supported",
|
569
|
+
loc
|
570
|
+
)
|
571
|
+
|
572
|
+
return self.factory.create_while_loop(test, body, loc)
|
573
|
+
|
574
|
+
# For loops
|
575
|
+
elif isinstance(node, ast.For):
|
576
|
+
target = self._convert_expression(node.target)
|
577
|
+
iter_expr = self._convert_expression(node.iter)
|
578
|
+
body = [self._convert_statement(stmt) for stmt in node.body]
|
579
|
+
loc = self._get_location(node)
|
580
|
+
|
581
|
+
# Note: We're ignoring the else clause for now
|
582
|
+
if node.orelse:
|
583
|
+
self.error_handler.add_error(
|
584
|
+
ErrorType.PARSER_ERROR,
|
585
|
+
"For-else clauses are not supported",
|
586
|
+
loc
|
587
|
+
)
|
588
|
+
|
589
|
+
return self.factory.create_for_loop(target, iter_expr, body, loc)
|
590
|
+
|
591
|
+
# Function definitions
|
592
|
+
elif isinstance(node, ast.FunctionDef):
|
593
|
+
loc = self._get_location(node)
|
594
|
+
|
595
|
+
# Convert arguments
|
596
|
+
args = []
|
597
|
+
for arg in node.args.args:
|
598
|
+
# Get annotation if present
|
599
|
+
annotation = None
|
600
|
+
if arg.annotation:
|
601
|
+
annotation = self._convert_expression(arg.annotation)
|
602
|
+
|
603
|
+
# Get default value if this argument has one
|
604
|
+
default = None
|
605
|
+
arg_idx = node.args.args.index(arg)
|
606
|
+
defaults_offset = len(node.args.args) - len(node.args.defaults)
|
607
|
+
if arg_idx >= defaults_offset and node.args.defaults:
|
608
|
+
default_idx = arg_idx - defaults_offset
|
609
|
+
if default_idx < len(node.args.defaults):
|
610
|
+
default_value = node.args.defaults[default_idx]
|
611
|
+
default = self._convert_expression(default_value)
|
612
|
+
|
613
|
+
args.append(Argument(arg.arg, annotation, default))
|
614
|
+
|
615
|
+
# Convert body
|
616
|
+
body = [self._convert_statement(stmt) for stmt in node.body]
|
617
|
+
|
618
|
+
# Convert return annotation if present
|
619
|
+
return_annotation = None
|
620
|
+
if node.returns:
|
621
|
+
return_annotation = self._convert_expression(node.returns)
|
622
|
+
|
623
|
+
return self.factory.create_function_definition(
|
624
|
+
node.name, args, body, return_annotation, loc
|
625
|
+
)
|
626
|
+
|
627
|
+
# Return statements
|
628
|
+
elif isinstance(node, ast.Return):
|
629
|
+
value = None
|
630
|
+
if node.value:
|
631
|
+
value = self._convert_expression(node.value)
|
632
|
+
return self.factory.create_return_statement(
|
633
|
+
value, self._get_location(node)
|
634
|
+
)
|
635
|
+
|
636
|
+
# Break statements
|
637
|
+
elif isinstance(node, ast.Break):
|
638
|
+
return self.factory.create_break_statement(
|
639
|
+
self._get_location(node)
|
640
|
+
)
|
641
|
+
|
642
|
+
# Continue statements
|
643
|
+
elif isinstance(node, ast.Continue):
|
644
|
+
return self.factory.create_continue_statement(
|
645
|
+
self._get_location(node)
|
646
|
+
)
|
647
|
+
|
648
|
+
# Pass statements - convert to empty expression statement
|
649
|
+
elif isinstance(node, ast.Pass):
|
650
|
+
return self.factory.create_expression_statement(
|
651
|
+
self.factory.create_none_literal(),
|
652
|
+
self._get_location(node)
|
653
|
+
)
|
654
|
+
|
655
|
+
# Import statements
|
656
|
+
elif isinstance(node, ast.Import):
|
657
|
+
loc = self._get_location(node)
|
658
|
+
# Create a list of assignments for each imported name
|
659
|
+
statements = []
|
660
|
+
|
661
|
+
for name in node.names:
|
662
|
+
# Create an identifier for the module
|
663
|
+
module_name = name.name
|
664
|
+
as_name = name.asname or module_name
|
665
|
+
|
666
|
+
# Create an assignment: as_name = module_name
|
667
|
+
target = self.factory.create_identifier(as_name, loc)
|
668
|
+
value = self.factory.create_identifier(f"<import:{module_name}>", loc)
|
669
|
+
|
670
|
+
statements.append(self.factory.create_assignment(target, value, loc))
|
671
|
+
|
672
|
+
# If there's only one statement, return it
|
673
|
+
if len(statements) == 1:
|
674
|
+
return statements[0]
|
675
|
+
|
676
|
+
# Otherwise, return the first one and add a warning
|
677
|
+
if len(statements) > 1:
|
678
|
+
self.error_handler.add_error(
|
679
|
+
ErrorType.PARSER_ERROR,
|
680
|
+
"Multiple imports in a single statement are not fully supported",
|
681
|
+
loc
|
682
|
+
)
|
683
|
+
|
684
|
+
return statements[0]
|
685
|
+
|
686
|
+
# Import from statements
|
687
|
+
elif isinstance(node, ast.ImportFrom):
|
688
|
+
loc = self._get_location(node)
|
689
|
+
module_name = node.module or ""
|
690
|
+
|
691
|
+
# Special case for "from vex import *"
|
692
|
+
if module_name == "vex" and any(name.name == "*" for name in node.names):
|
693
|
+
# Create a special identifier that represents "from vex import *"
|
694
|
+
return self.factory.create_expression_statement(
|
695
|
+
self.factory.create_identifier("<import:vex:*>", loc),
|
696
|
+
loc
|
697
|
+
)
|
698
|
+
|
699
|
+
# For other import from statements, create assignments
|
700
|
+
statements = []
|
701
|
+
|
702
|
+
for name in node.names:
|
703
|
+
# Create an identifier for the imported name
|
704
|
+
imported_name = name.name
|
705
|
+
as_name = name.asname or imported_name
|
706
|
+
|
707
|
+
# Create an assignment: as_name = module_name.imported_name
|
708
|
+
target = self.factory.create_identifier(as_name, loc)
|
709
|
+
value = self.factory.create_identifier(f"<import:{module_name}.{imported_name}>", loc)
|
710
|
+
|
711
|
+
statements.append(self.factory.create_assignment(target, value, loc))
|
712
|
+
|
713
|
+
# If there's only one statement, return it
|
714
|
+
if len(statements) == 1:
|
715
|
+
return statements[0]
|
716
|
+
|
717
|
+
# Otherwise, return the first one and add a warning
|
718
|
+
if len(statements) > 1:
|
719
|
+
self.error_handler.add_error(
|
720
|
+
ErrorType.PARSER_ERROR,
|
721
|
+
"Multiple imports in a single statement are not fully supported",
|
722
|
+
loc
|
723
|
+
)
|
724
|
+
|
725
|
+
return statements[0]
|
726
|
+
|
727
|
+
# Class definitions - not supported yet
|
728
|
+
elif isinstance(node, ast.ClassDef):
|
729
|
+
loc = self._get_location(node)
|
730
|
+
self.error_handler.add_error(
|
731
|
+
ErrorType.PARSER_ERROR,
|
732
|
+
"Class definitions are not supported",
|
733
|
+
loc
|
734
|
+
)
|
735
|
+
# Create a placeholder expression statement
|
736
|
+
return self.factory.create_expression_statement(
|
737
|
+
self.factory.create_identifier(
|
738
|
+
f"<class:{node.name}>",
|
739
|
+
loc
|
740
|
+
),
|
741
|
+
loc
|
742
|
+
)
|
743
|
+
|
744
|
+
# Fallback for unsupported nodes
|
745
|
+
self.error_handler.add_error(
|
746
|
+
ErrorType.PARSER_ERROR,
|
747
|
+
f"Unsupported statement type: {type(node).__name__}",
|
748
|
+
self._get_location(node)
|
749
|
+
)
|
750
|
+
# Return a simple expression statement as fallback
|
751
|
+
return self.factory.create_expression_statement(
|
752
|
+
self.factory.create_identifier(
|
753
|
+
f"<unsupported:{type(node).__name__}>",
|
754
|
+
self._get_location(node)
|
755
|
+
),
|
756
|
+
self._get_location(node)
|
757
|
+
)
|
758
|
+
|
759
|
+
def parse(self) -> Program:
|
760
|
+
"""Parse the Python source code and return a VEX AST."""
|
761
|
+
try:
|
762
|
+
# Dedent the source code to remove whitespace
|
763
|
+
dedented_source = textwrap.dedent(self.source)
|
764
|
+
|
765
|
+
# Parse the Python code with modern features
|
766
|
+
self._py_ast = ast.parse(
|
767
|
+
dedented_source,
|
768
|
+
filename=self.filename,
|
769
|
+
feature_version=(3, 8) # Explicitly use Python 3.8+ features
|
770
|
+
)
|
771
|
+
|
772
|
+
# Convert the module body to VEX statements
|
773
|
+
body = [self._convert_statement(stmt) for stmt in self._py_ast.body]
|
774
|
+
|
775
|
+
# Create and return the program node
|
776
|
+
return self.factory.create_program(body)
|
777
|
+
|
778
|
+
except SyntaxError as e:
|
779
|
+
# Convert Python SyntaxError to VexSyntaxError
|
780
|
+
loc = SourceLocation(
|
781
|
+
line=e.lineno or 1,
|
782
|
+
column=e.offset or 1,
|
783
|
+
filename=e.filename or self.filename
|
784
|
+
)
|
785
|
+
if hasattr(e, 'end_lineno') and e.end_lineno is not None and \
|
786
|
+
hasattr(e, 'end_offset') and e.end_offset is not None:
|
787
|
+
loc.end_line = e.end_lineno
|
788
|
+
loc.end_column = e.end_offset
|
789
|
+
|
790
|
+
self.error_handler.add_error(
|
791
|
+
ErrorType.PARSER_ERROR,
|
792
|
+
f"Syntax error: {e.msg}",
|
793
|
+
loc
|
794
|
+
)
|
795
|
+
|
796
|
+
# Only raise if the error handler is configured to do so
|
797
|
+
if self.error_handler._raise_on_error:
|
798
|
+
raise VexSyntaxError(f"Syntax error: {e.msg}", loc) from e
|
799
|
+
|
800
|
+
# Return an empty program if we're not raising
|
801
|
+
return self.factory.create_program([])
|
802
|
+
|
803
|
+
except Exception as e:
|
804
|
+
# Handle other parsing errors
|
805
|
+
self.error_handler.add_error(
|
806
|
+
ErrorType.PARSER_ERROR,
|
807
|
+
f"Failed to parse Python code: {str(e)}",
|
808
|
+
SourceLocation(1, 1, self.filename)
|
809
|
+
)
|
810
|
+
raise VexSyntaxError(
|
811
|
+
f"Failed to parse Python code: {str(e)}",
|
812
|
+
SourceLocation(1, 1, self.filename)
|
813
|
+
) from e
|
814
|
+
|
815
|
+
# Convenience functions
|
816
|
+
def parse_string(source: str, filename: str = "<string>",
|
817
|
+
error_handler: Optional[ErrorHandler] = None) -> Program:
|
818
|
+
"""Parse Python code from a string."""
|
819
|
+
parser = PythonParser(source, filename, error_handler)
|
820
|
+
return parser.parse()
|
821
|
+
|
822
|
+
def parse_file(filepath: str, error_handler: Optional[ErrorHandler] = None) -> Program:
|
823
|
+
"""Parse Python code from a file."""
|
824
|
+
try:
|
825
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
826
|
+
source = f.read()
|
827
|
+
return parse_string(source, filepath, error_handler)
|
828
|
+
except FileNotFoundError:
|
829
|
+
raise
|
830
|
+
except IOError as e:
|
831
|
+
raise IOError(f"Error reading file {filepath}: {e}")
|