vex-ast 0.2.2__tar.gz → 0.2.4__tar.gz
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-0.2.2/vex_ast.egg-info → vex_ast-0.2.4}/PKG-INFO +1 -1
- {vex_ast-0.2.2 → vex_ast-0.2.4}/pyproject.toml +1 -1
- {vex_ast-0.2.2 → vex_ast-0.2.4}/setup.py +1 -1
- vex_ast-0.2.4/tests/test_comprehensive_integration.py +338 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_parser.py +5 -4
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/expressions.py +44 -1
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/interfaces.py +16 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/vex_nodes.py +15 -2
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/factory.py +13 -2
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/python_parser.py +88 -27
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/signature.py +49 -1
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/json_deserializer.py +13 -2
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/schema.py +3 -4
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/errors.py +2 -2
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/base.py +4 -1
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/printer.py +2 -1
- {vex_ast-0.2.2 → vex_ast-0.2.4/vex_ast.egg-info}/PKG-INFO +1 -1
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast.egg-info/SOURCES.txt +1 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/LICENSE +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/MANIFEST.in +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/pytest.ini +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/requirements.txt +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/setup.cfg +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/conftest.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_core.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_integration.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_literals.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_navigator.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_registry.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_serialization.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_statements.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_vex_nodes.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/tests/test_visitors.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/READMEAPI.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/core.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/literals.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/navigator.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/operators.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/statements.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/ast/validators.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/interfaces.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/parser/strategies.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/api.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/categories.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/display.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/drivetrain.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/initialize.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/motor.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/sensors.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/functions/timing.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/language_map.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/registry.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/simulation_behavior.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/registry/validation.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/serialization/json_serializer.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/base.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/enums.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/objects.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/primitives.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/types/type_checker.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/source_location.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/utils/type_definitions.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/README.md +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/__init__.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/analyzer.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast/visitors/transformer.py +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast.egg-info/dependency_links.txt +0 -0
- {vex_ast-0.2.2 → vex_ast-0.2.4}/vex_ast.egg-info/top_level.txt +0 -0
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
2
2
|
|
3
3
|
setup(
|
4
4
|
name='vex_ast',
|
5
|
-
version='0.2.
|
5
|
+
version='0.2.4',
|
6
6
|
description='A Python package for generating Abstract Syntax Trees for VEX V5 code.',
|
7
7
|
long_description=open('README.md').read(),
|
8
8
|
long_description_content_type='text/markdown',
|
@@ -0,0 +1,338 @@
|
|
1
|
+
"""
|
2
|
+
Comprehensive integration tests for the vex_ast package.
|
3
|
+
|
4
|
+
These tests ensure that all API endpoints work together correctly in various scenarios.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import json
|
9
|
+
import tempfile
|
10
|
+
import pytest
|
11
|
+
from typing import Dict, Any
|
12
|
+
|
13
|
+
from vex_ast import (
|
14
|
+
parse_string,
|
15
|
+
parse_file,
|
16
|
+
ErrorHandler,
|
17
|
+
VexSyntaxError,
|
18
|
+
VexAstError,
|
19
|
+
PrintVisitor,
|
20
|
+
NodeCounter,
|
21
|
+
VariableCollector,
|
22
|
+
create_navigator,
|
23
|
+
serialize_ast_to_dict,
|
24
|
+
serialize_ast_to_json,
|
25
|
+
deserialize_ast_from_dict,
|
26
|
+
deserialize_ast_from_json,
|
27
|
+
generate_ast_schema,
|
28
|
+
export_schema_to_file,
|
29
|
+
registry_api
|
30
|
+
)
|
31
|
+
from vex_ast.ast.vex_nodes import VexAPICall
|
32
|
+
from vex_ast.ast.validators import validate_vex_functions
|
33
|
+
|
34
|
+
|
35
|
+
class TestComprehensiveIntegration:
|
36
|
+
"""Comprehensive integration tests for the vex_ast package."""
|
37
|
+
|
38
|
+
def test_parse_file_functionality(self):
|
39
|
+
"""Test the parse_file functionality with a temporary file."""
|
40
|
+
# Create a temporary file with VEX code
|
41
|
+
with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_file:
|
42
|
+
temp_file.write("""
|
43
|
+
# Define a motor
|
44
|
+
motor1 = Motor(PORT1)
|
45
|
+
|
46
|
+
# Define a function
|
47
|
+
def move_forward(speed):
|
48
|
+
motor1.spin(FORWARD, speed, PERCENT)
|
49
|
+
wait(1, SECONDS)
|
50
|
+
motor1.stop()
|
51
|
+
return True
|
52
|
+
|
53
|
+
# Call the function
|
54
|
+
success = move_forward(50)
|
55
|
+
""")
|
56
|
+
temp_file_path = temp_file.name
|
57
|
+
|
58
|
+
try:
|
59
|
+
# Parse the file
|
60
|
+
ast = parse_file(temp_file_path)
|
61
|
+
|
62
|
+
# Verify the AST structure
|
63
|
+
assert ast is not None
|
64
|
+
assert len(ast.body) > 0
|
65
|
+
|
66
|
+
# Find function definitions
|
67
|
+
func_defs = [node for node in ast.body if node.__class__.__name__ == "FunctionDefinition"]
|
68
|
+
assert len(func_defs) == 1
|
69
|
+
assert func_defs[0].name == "move_forward"
|
70
|
+
|
71
|
+
# Test that visitors work with the parsed AST
|
72
|
+
printer = PrintVisitor()
|
73
|
+
result = printer.visit(ast)
|
74
|
+
assert isinstance(result, str)
|
75
|
+
assert len(result) > 0
|
76
|
+
|
77
|
+
counter = NodeCounter()
|
78
|
+
count = counter.visit(ast)
|
79
|
+
assert count > 10
|
80
|
+
|
81
|
+
collector = VariableCollector()
|
82
|
+
variables = collector.visit(ast)
|
83
|
+
assert "motor1" in variables
|
84
|
+
assert "speed" in variables
|
85
|
+
|
86
|
+
finally:
|
87
|
+
# Clean up the temporary file
|
88
|
+
os.unlink(temp_file_path)
|
89
|
+
|
90
|
+
def test_error_handling_integration(self):
|
91
|
+
"""Test error handling integration with parsing and visitors."""
|
92
|
+
# Create an error handler that doesn't raise exceptions
|
93
|
+
error_handler = ErrorHandler(raise_on_error=False)
|
94
|
+
|
95
|
+
# Parse code with syntax errors
|
96
|
+
code_with_errors = """
|
97
|
+
def invalid_function(
|
98
|
+
# Missing closing parenthesis
|
99
|
+
print("This will cause an error")
|
100
|
+
"""
|
101
|
+
|
102
|
+
# This should not raise an exception due to raise_on_error=False
|
103
|
+
ast = parse_string(code_with_errors, error_handler=error_handler)
|
104
|
+
|
105
|
+
# Verify that errors were collected
|
106
|
+
assert error_handler.has_errors()
|
107
|
+
errors = error_handler.get_errors()
|
108
|
+
assert len(errors) > 0
|
109
|
+
|
110
|
+
# Try with a different error handler that raises exceptions
|
111
|
+
error_handler_strict = ErrorHandler(raise_on_error=True)
|
112
|
+
|
113
|
+
# This should raise a VexSyntaxError
|
114
|
+
with pytest.raises(VexSyntaxError):
|
115
|
+
parse_string(code_with_errors, error_handler=error_handler_strict)
|
116
|
+
|
117
|
+
# Test error handling with valid code but invalid VEX API calls
|
118
|
+
invalid_vex_code = """
|
119
|
+
motor1 = Motor(PORT1)
|
120
|
+
# Invalid: wrong arguments to spin
|
121
|
+
motor1.spin(INVALID_DIRECTION, "not_a_number", "not_a_unit")
|
122
|
+
"""
|
123
|
+
|
124
|
+
# Parse the code (should succeed syntactically)
|
125
|
+
ast = parse_string(invalid_vex_code)
|
126
|
+
|
127
|
+
# Validate VEX functions (should find errors)
|
128
|
+
validation_errors = validate_vex_functions(ast)
|
129
|
+
assert len(validation_errors) > 0
|
130
|
+
|
131
|
+
def test_end_to_end_workflow(self):
|
132
|
+
"""Test an end-to-end workflow combining multiple API functions."""
|
133
|
+
# 1. Parse a complex program
|
134
|
+
code = """
|
135
|
+
# Initialize motors
|
136
|
+
left_motor = Motor(PORT1)
|
137
|
+
right_motor = Motor(PORT2)
|
138
|
+
|
139
|
+
# Define a function to drive forward
|
140
|
+
def drive_forward(speed, time_ms):
|
141
|
+
# Set both motors to the specified speed
|
142
|
+
left_motor.set_velocity(speed, PERCENT)
|
143
|
+
right_motor.set_velocity(speed, PERCENT)
|
144
|
+
|
145
|
+
# Start the motors
|
146
|
+
left_motor.spin(FORWARD)
|
147
|
+
right_motor.spin(FORWARD)
|
148
|
+
|
149
|
+
# Wait for the specified time
|
150
|
+
wait(time_ms, MSEC)
|
151
|
+
|
152
|
+
# Stop the motors
|
153
|
+
left_motor.stop()
|
154
|
+
right_motor.stop()
|
155
|
+
|
156
|
+
return True
|
157
|
+
|
158
|
+
# Define a function to turn
|
159
|
+
def turn(direction, speed, angle):
|
160
|
+
if direction == LEFT:
|
161
|
+
left_motor.set_velocity(-speed, PERCENT)
|
162
|
+
right_motor.set_velocity(speed, PERCENT)
|
163
|
+
else:
|
164
|
+
left_motor.set_velocity(speed, PERCENT)
|
165
|
+
right_motor.set_velocity(-speed, PERCENT)
|
166
|
+
|
167
|
+
left_motor.spin(FORWARD)
|
168
|
+
right_motor.spin(FORWARD)
|
169
|
+
|
170
|
+
# Wait until the robot has turned the specified angle
|
171
|
+
wait_until(gyro.rotation() >= angle)
|
172
|
+
|
173
|
+
left_motor.stop()
|
174
|
+
right_motor.stop()
|
175
|
+
|
176
|
+
return True
|
177
|
+
|
178
|
+
# Main program
|
179
|
+
def main():
|
180
|
+
# Display welcome message
|
181
|
+
brain.screen.print("Robot starting...")
|
182
|
+
wait(1, SECONDS)
|
183
|
+
|
184
|
+
# Drive forward at 50% speed for 2 seconds
|
185
|
+
success = drive_forward(50, 2000)
|
186
|
+
|
187
|
+
if success:
|
188
|
+
# Turn right 90 degrees
|
189
|
+
turn(RIGHT, 30, 90)
|
190
|
+
|
191
|
+
# Drive forward again
|
192
|
+
drive_forward(50, 1000)
|
193
|
+
|
194
|
+
brain.screen.print("Mission completed!")
|
195
|
+
else:
|
196
|
+
brain.screen.print("Drive failed!")
|
197
|
+
|
198
|
+
return 0
|
199
|
+
"""
|
200
|
+
|
201
|
+
# 2. Parse the code
|
202
|
+
ast = parse_string(code)
|
203
|
+
|
204
|
+
# 3. Use a visitor to analyze the AST
|
205
|
+
counter = NodeCounter()
|
206
|
+
node_count = counter.visit(ast)
|
207
|
+
assert node_count > 50 # Complex program should have many nodes
|
208
|
+
|
209
|
+
# 4. Use the navigator to find specific nodes
|
210
|
+
navigator = create_navigator(ast)
|
211
|
+
|
212
|
+
# Find all function definitions
|
213
|
+
func_defs = navigator.find_function_definitions()
|
214
|
+
assert len(func_defs) == 3
|
215
|
+
func_names = [func.name for func in func_defs]
|
216
|
+
assert "drive_forward" in func_names
|
217
|
+
assert "turn" in func_names
|
218
|
+
assert "main" in func_names
|
219
|
+
|
220
|
+
# Find all VEX API calls
|
221
|
+
vex_calls = navigator.find_vex_api_calls()
|
222
|
+
assert len(vex_calls) > 5
|
223
|
+
|
224
|
+
# 5. Serialize the AST to JSON
|
225
|
+
json_str = serialize_ast_to_json(ast)
|
226
|
+
assert isinstance(json_str, str)
|
227
|
+
|
228
|
+
# 6. Deserialize back to an AST
|
229
|
+
deserialized_ast = deserialize_ast_from_json(json_str)
|
230
|
+
|
231
|
+
# 7. Verify the deserialized AST has the same structure
|
232
|
+
deserialized_func_defs = [node for node in deserialized_ast.body
|
233
|
+
if node.__class__.__name__ == "FunctionDefinition"]
|
234
|
+
assert len(deserialized_func_defs) == 3
|
235
|
+
deserialized_func_names = [func.name for func in deserialized_func_defs]
|
236
|
+
assert set(deserialized_func_names) == set(func_names)
|
237
|
+
|
238
|
+
# 8. Use a visitor on the deserialized AST
|
239
|
+
collector = VariableCollector()
|
240
|
+
variables = collector.visit(deserialized_ast)
|
241
|
+
assert "left_motor" in variables
|
242
|
+
assert "right_motor" in variables
|
243
|
+
assert "speed" in variables
|
244
|
+
assert "direction" in variables
|
245
|
+
|
246
|
+
def test_schema_export_functionality(self):
|
247
|
+
"""Test the schema export functionality."""
|
248
|
+
# Generate the schema
|
249
|
+
schema = generate_ast_schema()
|
250
|
+
assert isinstance(schema, dict)
|
251
|
+
|
252
|
+
# Export the schema to a temporary file
|
253
|
+
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as temp_file:
|
254
|
+
temp_file_path = temp_file.name
|
255
|
+
|
256
|
+
try:
|
257
|
+
# Export the schema
|
258
|
+
export_schema_to_file(schema, temp_file_path)
|
259
|
+
|
260
|
+
# Verify the file exists and contains valid JSON
|
261
|
+
assert os.path.exists(temp_file_path)
|
262
|
+
|
263
|
+
with open(temp_file_path, 'r') as f:
|
264
|
+
loaded_schema = json.load(f)
|
265
|
+
|
266
|
+
# Check that it's the same schema
|
267
|
+
assert loaded_schema["$schema"] == schema["$schema"]
|
268
|
+
assert "definitions" in loaded_schema
|
269
|
+
assert set(loaded_schema["definitions"].keys()) == set(schema["definitions"].keys())
|
270
|
+
|
271
|
+
finally:
|
272
|
+
# Clean up
|
273
|
+
if os.path.exists(temp_file_path):
|
274
|
+
os.unlink(temp_file_path)
|
275
|
+
|
276
|
+
def test_edge_cases(self):
|
277
|
+
"""Test edge cases in parsing and serialization."""
|
278
|
+
# Test parsing an empty program
|
279
|
+
empty_ast = parse_string("")
|
280
|
+
assert empty_ast is not None
|
281
|
+
assert len(empty_ast.body) == 0
|
282
|
+
|
283
|
+
# Test parsing a program with only comments
|
284
|
+
comments_ast = parse_string("# This is a comment\n# Another comment")
|
285
|
+
assert comments_ast is not None
|
286
|
+
assert len(comments_ast.body) == 0
|
287
|
+
|
288
|
+
# Test parsing a program with unusual whitespace
|
289
|
+
whitespace_code = """
|
290
|
+
|
291
|
+
|
292
|
+
x = 42
|
293
|
+
|
294
|
+
|
295
|
+
y = x + 10
|
296
|
+
|
297
|
+
|
298
|
+
"""
|
299
|
+
whitespace_ast = parse_string(whitespace_code)
|
300
|
+
assert whitespace_ast is not None
|
301
|
+
assert len(whitespace_ast.body) == 2
|
302
|
+
|
303
|
+
# Test serializing and deserializing an empty program
|
304
|
+
empty_json = serialize_ast_to_json(empty_ast)
|
305
|
+
deserialized_empty = deserialize_ast_from_json(empty_json)
|
306
|
+
assert deserialized_empty is not None
|
307
|
+
assert len(deserialized_empty.body) == 0
|
308
|
+
|
309
|
+
# Test with a program containing all types of literals
|
310
|
+
literals_code = """
|
311
|
+
# Number literals
|
312
|
+
int_val = 42
|
313
|
+
float_val = 3.14
|
314
|
+
|
315
|
+
# String literals
|
316
|
+
str_val = "Hello, world!"
|
317
|
+
str_val2 = 'Single quotes'
|
318
|
+
|
319
|
+
# Boolean literals
|
320
|
+
bool_val1 = True
|
321
|
+
bool_val2 = False
|
322
|
+
|
323
|
+
# None literal
|
324
|
+
none_val = None
|
325
|
+
"""
|
326
|
+
|
327
|
+
literals_ast = parse_string(literals_code)
|
328
|
+
assert literals_ast is not None
|
329
|
+
assert len(literals_ast.body) > 0
|
330
|
+
|
331
|
+
# Serialize and deserialize
|
332
|
+
literals_json = serialize_ast_to_json(literals_ast)
|
333
|
+
deserialized_literals = deserialize_ast_from_json(literals_json)
|
334
|
+
|
335
|
+
# Verify the literals were preserved
|
336
|
+
collector = VariableCollector()
|
337
|
+
variables = collector.visit(deserialized_literals)
|
338
|
+
assert len(variables) == 0 # All are assignments, no references
|
@@ -58,9 +58,10 @@ class TestParser:
|
|
58
58
|
code = "if x ==" # Incomplete if statement
|
59
59
|
error_handler = ErrorHandler(raise_on_error=False)
|
60
60
|
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
# Should not raise an exception when raise_on_error=False
|
62
|
+
ast = parse_string(code, error_handler=error_handler)
|
63
|
+
|
64
|
+
# But should collect errors
|
64
65
|
assert error_handler.has_errors()
|
65
66
|
errors = error_handler.get_errors()
|
66
|
-
assert len(errors) >= 1
|
67
|
+
assert len(errors) >= 1
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from typing import Dict, List, Optional, Union, cast, Any
|
4
4
|
|
5
|
-
from .interfaces import IAstNode, IExpression, IVisitor, T_VisitorResult, IIdentifier, IFunctionCall
|
5
|
+
from .interfaces import IAstNode, IExpression, IVisitor, T_VisitorResult, IIdentifier, IFunctionCall, IConditionalExpression
|
6
6
|
from .core import Expression
|
7
7
|
from .operators import Operator
|
8
8
|
from ..utils.source_location import SourceLocation
|
@@ -173,6 +173,49 @@ class KeywordArgument(Expression):
|
|
173
173
|
"""Get the keyword value."""
|
174
174
|
return self.value
|
175
175
|
|
176
|
+
class ConditionalExpression(Expression, IConditionalExpression):
|
177
|
+
"""A conditional expression (ternary operator, e.g., a if condition else b)."""
|
178
|
+
|
179
|
+
_fields = ('condition', 'true_expr', 'false_expr')
|
180
|
+
|
181
|
+
def __init__(self, condition: IExpression, true_expr: IExpression, false_expr: IExpression,
|
182
|
+
location: Optional[SourceLocation] = None):
|
183
|
+
super().__init__(location)
|
184
|
+
self.condition = condition
|
185
|
+
self.true_expr = true_expr
|
186
|
+
self.false_expr = false_expr
|
187
|
+
|
188
|
+
# Set parent references
|
189
|
+
if isinstance(condition, Expression):
|
190
|
+
condition.set_parent(self)
|
191
|
+
if isinstance(true_expr, Expression):
|
192
|
+
true_expr.set_parent(self)
|
193
|
+
if isinstance(false_expr, Expression):
|
194
|
+
false_expr.set_parent(self)
|
195
|
+
|
196
|
+
def get_children(self) -> List[IAstNode]:
|
197
|
+
"""Get child nodes."""
|
198
|
+
return [
|
199
|
+
cast(IAstNode, self.condition),
|
200
|
+
cast(IAstNode, self.true_expr),
|
201
|
+
cast(IAstNode, self.false_expr)
|
202
|
+
]
|
203
|
+
|
204
|
+
def accept(self, visitor: IVisitor[T_VisitorResult]) -> T_VisitorResult:
|
205
|
+
return visitor.visit_conditionalexpression(self)
|
206
|
+
|
207
|
+
def get_condition(self) -> IExpression:
|
208
|
+
"""Get the condition expression."""
|
209
|
+
return self.condition
|
210
|
+
|
211
|
+
def get_true_expression(self) -> IExpression:
|
212
|
+
"""Get the expression to evaluate if condition is true."""
|
213
|
+
return self.true_expr
|
214
|
+
|
215
|
+
def get_false_expression(self) -> IExpression:
|
216
|
+
"""Get the expression to evaluate if condition is false."""
|
217
|
+
return self.false_expr
|
218
|
+
|
176
219
|
class FunctionCall(Expression, IFunctionCall):
|
177
220
|
"""A function call."""
|
178
221
|
|
@@ -84,6 +84,22 @@ class IIdentifier(IExpression, Protocol):
|
|
84
84
|
"""Get the identifier name."""
|
85
85
|
...
|
86
86
|
|
87
|
+
@runtime_checkable
|
88
|
+
class IConditionalExpression(IExpression, Protocol):
|
89
|
+
"""Protocol for conditional expression (ternary operator) nodes."""
|
90
|
+
|
91
|
+
def get_condition(self) -> IExpression:
|
92
|
+
"""Get the condition expression."""
|
93
|
+
...
|
94
|
+
|
95
|
+
def get_true_expression(self) -> IExpression:
|
96
|
+
"""Get the expression to evaluate if condition is true."""
|
97
|
+
...
|
98
|
+
|
99
|
+
def get_false_expression(self) -> IExpression:
|
100
|
+
"""Get the expression to evaluate if condition is false."""
|
101
|
+
...
|
102
|
+
|
87
103
|
@runtime_checkable
|
88
104
|
class IFunctionCall(IExpression, Protocol):
|
89
105
|
"""Protocol for function call nodes."""
|
@@ -91,8 +91,21 @@ class VexAPICall(FunctionCall):
|
|
91
91
|
return False, self._validation_error
|
92
92
|
|
93
93
|
# Convert args and kwargs to appropriate format
|
94
|
-
arg_values = [
|
95
|
-
|
94
|
+
arg_values = []
|
95
|
+
for arg in self.args:
|
96
|
+
# For string literals, use their actual string value for validation
|
97
|
+
if hasattr(arg, 'value') and hasattr(arg, '__class__') and arg.__class__.__name__ == 'StringLiteral':
|
98
|
+
arg_values.append(arg.value)
|
99
|
+
else:
|
100
|
+
arg_values.append(arg)
|
101
|
+
|
102
|
+
kwarg_values = {}
|
103
|
+
for kw in (self.keywords or []):
|
104
|
+
# For string literals, use their actual string value for validation
|
105
|
+
if hasattr(kw.value, 'value') and hasattr(kw.value, '__class__') and kw.value.__class__.__name__ == 'StringLiteral':
|
106
|
+
kwarg_values[kw.name] = kw.value.value
|
107
|
+
else:
|
108
|
+
kwarg_values[kw.name] = kw.value
|
96
109
|
|
97
110
|
# Validate against the signature
|
98
111
|
valid, error = signature.validate_arguments(arg_values, kwarg_values)
|
@@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Type, Union, cast, List
|
|
4
4
|
|
5
5
|
from ..ast.core import Expression, Program, Statement
|
6
6
|
from ..ast.expressions import (
|
7
|
-
AttributeAccess, BinaryOperation, FunctionCall, Identifier, KeywordArgument,
|
7
|
+
AttributeAccess, BinaryOperation, ConditionalExpression, FunctionCall, Identifier, KeywordArgument,
|
8
8
|
UnaryOperation, VariableReference
|
9
9
|
)
|
10
10
|
from ..ast.literals import (
|
@@ -77,6 +77,11 @@ class NodeFactory:
|
|
77
77
|
"""Create a unary operation node."""
|
78
78
|
return UnaryOperation(op, operand, location)
|
79
79
|
|
80
|
+
def create_conditional_expression(self, condition: Expression, true_expr: Expression, false_expr: Expression,
|
81
|
+
location: Optional[SourceLocation] = None) -> ConditionalExpression:
|
82
|
+
"""Create a conditional expression (ternary operator) node."""
|
83
|
+
return ConditionalExpression(condition, true_expr, false_expr, location)
|
84
|
+
|
80
85
|
def create_function_call(self, function: Expression, args: List[Expression] = None,
|
81
86
|
keywords: List[KeywordArgument] = None,
|
82
87
|
location: Optional[SourceLocation] = None) -> FunctionCall:
|
@@ -123,6 +128,12 @@ class NodeFactory:
|
|
123
128
|
"""Create a function definition node."""
|
124
129
|
return FunctionDefinition(name, args, body, return_annotation, location)
|
125
130
|
|
131
|
+
def create_argument(self, name: str, annotation: Optional[Expression] = None,
|
132
|
+
default: Optional[Expression] = None,
|
133
|
+
location: Optional[SourceLocation] = None) -> Argument:
|
134
|
+
"""Create an argument node."""
|
135
|
+
return Argument(name, annotation, default, location)
|
136
|
+
|
126
137
|
def create_return_statement(self, value: Optional[Expression] = None,
|
127
138
|
location: Optional[SourceLocation] = None) -> ReturnStatement:
|
128
139
|
"""Create a return statement node."""
|
@@ -176,4 +187,4 @@ class NodeFactory:
|
|
176
187
|
return Program(body, location)
|
177
188
|
|
178
189
|
# Global factory instance for simple use cases
|
179
|
-
default_factory = NodeFactory()
|
190
|
+
default_factory = NodeFactory()
|
@@ -294,21 +294,12 @@ class PythonParser(BaseParser):
|
|
294
294
|
op_name = op_type.__name__
|
295
295
|
|
296
296
|
op_map = {
|
297
|
-
'And':
|
298
|
-
'Or':
|
297
|
+
'And': Operator.LOGICAL_AND,
|
298
|
+
'Or': Operator.LOGICAL_OR
|
299
299
|
}
|
300
300
|
|
301
301
|
if op_name in op_map:
|
302
|
-
|
303
|
-
vex_op = PYTHON_COMP_OP_MAP.get(op_str)
|
304
|
-
|
305
|
-
if not vex_op:
|
306
|
-
self.error_handler.add_error(
|
307
|
-
ErrorType.PARSER_ERROR,
|
308
|
-
f"Unsupported boolean operator: {op_name}",
|
309
|
-
loc
|
310
|
-
)
|
311
|
-
vex_op = Operator.LOGICAL_AND # Fallback
|
302
|
+
vex_op = op_map[op_name]
|
312
303
|
else:
|
313
304
|
self.error_handler.add_error(
|
314
305
|
ErrorType.PARSER_ERROR,
|
@@ -329,6 +320,15 @@ class PythonParser(BaseParser):
|
|
329
320
|
|
330
321
|
return result
|
331
322
|
|
323
|
+
# Conditional expressions (ternary operators)
|
324
|
+
elif isinstance(node, ast.IfExp):
|
325
|
+
loc = self._get_location(node)
|
326
|
+
test = self._convert_expression(node.test)
|
327
|
+
body = self._convert_expression(node.body)
|
328
|
+
orelse = self._convert_expression(node.orelse)
|
329
|
+
|
330
|
+
return self.factory.create_conditional_expression(test, body, orelse, loc)
|
331
|
+
|
332
332
|
# List literals
|
333
333
|
elif isinstance(node, ast.List) or isinstance(node, ast.Tuple):
|
334
334
|
# We don't have a dedicated list/tuple node, so use function call
|
@@ -607,22 +607,77 @@ class PythonParser(BaseParser):
|
|
607
607
|
self._get_location(node)
|
608
608
|
)
|
609
609
|
|
610
|
-
# Import statements
|
611
|
-
elif isinstance(node,
|
610
|
+
# Import statements
|
611
|
+
elif isinstance(node, ast.Import):
|
612
612
|
loc = self._get_location(node)
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
613
|
+
# Create a list of assignments for each imported name
|
614
|
+
statements = []
|
615
|
+
|
616
|
+
for name in node.names:
|
617
|
+
# Create an identifier for the module
|
618
|
+
module_name = name.name
|
619
|
+
as_name = name.asname or module_name
|
620
|
+
|
621
|
+
# Create an assignment: as_name = module_name
|
622
|
+
target = self.factory.create_identifier(as_name, loc)
|
623
|
+
value = self.factory.create_identifier(f"<import:{module_name}>", loc)
|
624
|
+
|
625
|
+
statements.append(self.factory.create_assignment(target, value, loc))
|
626
|
+
|
627
|
+
# If there's only one statement, return it
|
628
|
+
if len(statements) == 1:
|
629
|
+
return statements[0]
|
630
|
+
|
631
|
+
# Otherwise, return the first one and add a warning
|
632
|
+
if len(statements) > 1:
|
633
|
+
self.error_handler.add_error(
|
634
|
+
ErrorType.PARSER_ERROR,
|
635
|
+
"Multiple imports in a single statement are not fully supported",
|
622
636
|
loc
|
623
|
-
)
|
624
|
-
|
625
|
-
|
637
|
+
)
|
638
|
+
|
639
|
+
return statements[0]
|
640
|
+
|
641
|
+
# Import from statements
|
642
|
+
elif isinstance(node, ast.ImportFrom):
|
643
|
+
loc = self._get_location(node)
|
644
|
+
module_name = node.module or ""
|
645
|
+
|
646
|
+
# Special case for "from vex import *"
|
647
|
+
if module_name == "vex" and any(name.name == "*" for name in node.names):
|
648
|
+
# Create a special identifier that represents "from vex import *"
|
649
|
+
return self.factory.create_expression_statement(
|
650
|
+
self.factory.create_identifier("<import:vex:*>", loc),
|
651
|
+
loc
|
652
|
+
)
|
653
|
+
|
654
|
+
# For other import from statements, create assignments
|
655
|
+
statements = []
|
656
|
+
|
657
|
+
for name in node.names:
|
658
|
+
# Create an identifier for the imported name
|
659
|
+
imported_name = name.name
|
660
|
+
as_name = name.asname or imported_name
|
661
|
+
|
662
|
+
# Create an assignment: as_name = module_name.imported_name
|
663
|
+
target = self.factory.create_identifier(as_name, loc)
|
664
|
+
value = self.factory.create_identifier(f"<import:{module_name}.{imported_name}>", loc)
|
665
|
+
|
666
|
+
statements.append(self.factory.create_assignment(target, value, loc))
|
667
|
+
|
668
|
+
# If there's only one statement, return it
|
669
|
+
if len(statements) == 1:
|
670
|
+
return statements[0]
|
671
|
+
|
672
|
+
# Otherwise, return the first one and add a warning
|
673
|
+
if len(statements) > 1:
|
674
|
+
self.error_handler.add_error(
|
675
|
+
ErrorType.PARSER_ERROR,
|
676
|
+
"Multiple imports in a single statement are not fully supported",
|
677
|
+
loc
|
678
|
+
)
|
679
|
+
|
680
|
+
return statements[0]
|
626
681
|
|
627
682
|
# Class definitions - not supported yet
|
628
683
|
elif isinstance(node, ast.ClassDef):
|
@@ -692,7 +747,13 @@ class PythonParser(BaseParser):
|
|
692
747
|
f"Syntax error: {e.msg}",
|
693
748
|
loc
|
694
749
|
)
|
695
|
-
|
750
|
+
|
751
|
+
# Only raise if the error handler is configured to do so
|
752
|
+
if self.error_handler._raise_on_error:
|
753
|
+
raise VexSyntaxError(f"Syntax error: {e.msg}", loc) from e
|
754
|
+
|
755
|
+
# Return an empty program if we're not raising
|
756
|
+
return self.factory.create_program([])
|
696
757
|
|
697
758
|
except Exception as e:
|
698
759
|
# Handle other parsing errors
|
@@ -24,6 +24,11 @@ class VexFunctionParameter:
|
|
24
24
|
self.description = description
|
25
25
|
self.is_optional = default_value is not None
|
26
26
|
|
27
|
+
@property
|
28
|
+
def optional(self) -> bool:
|
29
|
+
"""Alias for is_optional for compatibility."""
|
30
|
+
return self.is_optional
|
31
|
+
|
27
32
|
def __str__(self) -> str:
|
28
33
|
mode_str = ""
|
29
34
|
if self.mode == ParameterMode.REFERENCE:
|
@@ -138,6 +143,49 @@ class VexFunctionSignature:
|
|
138
143
|
if param_name in kwargs:
|
139
144
|
return False, f"Duplicate argument '{param_name}' for {self.name}"
|
140
145
|
|
141
|
-
#
|
146
|
+
# Type checking for arguments
|
147
|
+
from ..types.type_checker import type_checker
|
148
|
+
from ..types.enums import EnumType
|
149
|
+
|
150
|
+
# Check positional arguments
|
151
|
+
for i, arg in enumerate(args):
|
152
|
+
if i >= len(self.parameters):
|
153
|
+
break
|
154
|
+
|
155
|
+
param = self.parameters[i]
|
156
|
+
expected_type = param.type
|
157
|
+
|
158
|
+
# Handle string literals for enum types
|
159
|
+
if isinstance(expected_type, EnumType) and isinstance(arg, str):
|
160
|
+
if arg not in expected_type.values:
|
161
|
+
return False, f"Invalid enum value '{arg}' for parameter '{param.name}' in {self.name}"
|
162
|
+
continue
|
163
|
+
|
164
|
+
# Handle other types
|
165
|
+
if hasattr(arg, 'get_type'):
|
166
|
+
arg_type = arg.get_type()
|
167
|
+
if arg_type and not type_checker.is_compatible(arg_type, expected_type):
|
168
|
+
return False, f"Type mismatch for parameter '{param.name}' in {self.name}: expected {expected_type}, got {arg_type}"
|
169
|
+
|
170
|
+
# Check keyword arguments
|
171
|
+
for kwarg_name, kwarg_value in kwargs.items():
|
172
|
+
# Find the parameter
|
173
|
+
param = next((p for p in self.parameters if p.name == kwarg_name), None)
|
174
|
+
if not param:
|
175
|
+
continue # Already checked for unknown kwargs above
|
176
|
+
|
177
|
+
expected_type = param.type
|
178
|
+
|
179
|
+
# Handle string literals for enum types
|
180
|
+
if isinstance(expected_type, EnumType) and isinstance(kwarg_value, str):
|
181
|
+
if kwarg_value not in expected_type.values:
|
182
|
+
return False, f"Invalid enum value '{kwarg_value}' for parameter '{param.name}' in {self.name}"
|
183
|
+
continue
|
184
|
+
|
185
|
+
# Handle other types
|
186
|
+
if hasattr(kwarg_value, 'get_type'):
|
187
|
+
kwarg_type = kwarg_value.get_type()
|
188
|
+
if kwarg_type and not type_checker.is_compatible(kwarg_type, expected_type):
|
189
|
+
return False, f"Type mismatch for parameter '{param.name}' in {self.name}: expected {expected_type}, got {kwarg_type}"
|
142
190
|
|
143
191
|
return True, None
|
@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Type, Union, cast
|
|
10
10
|
from ..ast.interfaces import IAstNode
|
11
11
|
from ..parser.factory import NodeFactory
|
12
12
|
from ..utils.source_location import SourceLocation
|
13
|
-
from ..utils.errors import ErrorHandler
|
13
|
+
from ..utils.errors import ErrorHandler, ErrorType
|
14
14
|
|
15
15
|
|
16
16
|
class DeserializationFactory:
|
@@ -153,6 +153,7 @@ class DeserializationFactory:
|
|
153
153
|
"WhileLoop": self.node_factory.create_while_loop,
|
154
154
|
"ForLoop": self.node_factory.create_for_loop,
|
155
155
|
"FunctionDefinition": self.node_factory.create_function_definition,
|
156
|
+
"Argument": self.node_factory.create_argument,
|
156
157
|
"ReturnStatement": self.node_factory.create_return_statement,
|
157
158
|
"BreakStatement": self.node_factory.create_break_statement,
|
158
159
|
"ContinueStatement": self.node_factory.create_continue_statement,
|
@@ -198,11 +199,21 @@ class DeserializationFactory:
|
|
198
199
|
except TypeError as e:
|
199
200
|
# If the factory method doesn't accept the kwargs, report an error
|
200
201
|
if self.error_handler:
|
201
|
-
self.error_handler.
|
202
|
+
self.error_handler.add_error(
|
203
|
+
error_type=ErrorType.INTERNAL_ERROR,
|
204
|
+
message=f"Failed to create {node_type}: {str(e)}"
|
205
|
+
)
|
202
206
|
raise ValueError(f"Failed to deserialize {node_type}: {str(e)}")
|
203
207
|
|
204
208
|
# Specific node creation methods for complex cases
|
205
209
|
|
210
|
+
def _create_attributeaccess(self, data: Dict[str, Any],
|
211
|
+
location: Optional[SourceLocation]) -> IAstNode:
|
212
|
+
"""Create an AttributeAccess node from serialized data."""
|
213
|
+
object_expr = self._deserialize_value(data.get("object"))
|
214
|
+
attribute = data.get("attribute", "")
|
215
|
+
return self.node_factory.create_attribute_access(object_expr, attribute, location)
|
216
|
+
|
206
217
|
def _create_program(self, data: Dict[str, Any],
|
207
218
|
location: Optional[SourceLocation]) -> IAstNode:
|
208
219
|
"""Create a Program node from serialized data."""
|
@@ -453,16 +453,15 @@ def _generate_definitions() -> Dict[str, Any]:
|
|
453
453
|
return definitions
|
454
454
|
|
455
455
|
|
456
|
-
def export_schema_to_file(filepath: str, indent: int = 2) -> None:
|
456
|
+
def export_schema_to_file(schema: Dict[str, Any], filepath: str, indent: int = 2) -> None:
|
457
457
|
"""
|
458
|
-
Save the
|
458
|
+
Save the schema to a file.
|
459
459
|
|
460
460
|
Args:
|
461
|
+
schema: The schema to save
|
461
462
|
filepath: The path to save the schema to
|
462
463
|
indent: The indentation level for pretty-printing
|
463
464
|
"""
|
464
|
-
schema = generate_ast_schema()
|
465
|
-
|
466
465
|
# Ensure the directory exists
|
467
466
|
os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
|
468
467
|
|
@@ -50,7 +50,7 @@ class ErrorObserver(Protocol[T_Error]):
|
|
50
50
|
class ErrorHandler:
|
51
51
|
"""Manages error collection and notification."""
|
52
52
|
|
53
|
-
def __init__(self, raise_on_error: bool =
|
53
|
+
def __init__(self, raise_on_error: bool = True):
|
54
54
|
self._errors: List[Error] = []
|
55
55
|
self._raise_on_error = raise_on_error
|
56
56
|
self._observers: List[ErrorObserver] = []
|
@@ -109,4 +109,4 @@ class VexSyntaxError(VexAstError):
|
|
109
109
|
|
110
110
|
def __init__(self, message: str, location: Optional[SourceLocation] = None):
|
111
111
|
self.location = location
|
112
|
-
super().__init__(message)
|
112
|
+
super().__init__(message)
|
@@ -47,6 +47,9 @@ class AstVisitor(Generic[T_VisitorResult], ABC):
|
|
47
47
|
def visit_unaryoperation(self, node: Any) -> T_VisitorResult:
|
48
48
|
return self.generic_visit(node)
|
49
49
|
|
50
|
+
def visit_conditionalexpression(self, node: Any) -> T_VisitorResult:
|
51
|
+
return self.generic_visit(node)
|
52
|
+
|
50
53
|
def visit_functioncall(self, node: Any) -> T_VisitorResult:
|
51
54
|
return self.generic_visit(node)
|
52
55
|
|
@@ -127,4 +130,4 @@ class TypedVisitorMixin:
|
|
127
130
|
method_name = self.node_type_to_method_name(type(node))
|
128
131
|
if hasattr(self, method_name):
|
129
132
|
return getattr(self, method_name)(node)
|
130
|
-
return self.generic_visit(node)
|
133
|
+
return self.generic_visit(node)
|
@@ -126,6 +126,7 @@ class PrintVisitor(AstVisitor[str]):
|
|
126
126
|
visit_variablereference = generic_visit
|
127
127
|
visit_binaryoperation = generic_visit
|
128
128
|
visit_unaryoperation = generic_visit
|
129
|
+
visit_conditionalexpression = generic_visit
|
129
130
|
visit_functioncall = generic_visit
|
130
131
|
visit_keywordargument = generic_visit
|
131
132
|
visit_expressionstatement = generic_visit
|
@@ -142,4 +143,4 @@ class PrintVisitor(AstVisitor[str]):
|
|
142
143
|
visit_motorcontrol = generic_visit
|
143
144
|
visit_sensorreading = generic_visit
|
144
145
|
visit_timingcontrol = generic_visit
|
145
|
-
visit_displayoutput = generic_visit
|
146
|
+
visit_displayoutput = generic_visit
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|