diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a54cdc8 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,41 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.12"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + # Install your project + dev dependencies (pytest lives here) + - name: Install dependencies + run: | + python -m pip install -e ".[dev]" + + # Sanity check (helps debug future CI issues) + - name: Verify environment + run: | + python -c "import sidewinder; print('sidewinder import OK')" + python -m pip show pytest + + - name: Run tests + run: python -m pytest -q \ No newline at end of file diff --git a/.python-version b/.python-version index c8cfe39..e4fba21 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.12 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8a07dc8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Transformer", + "type": "debugpy", + "request": "launch", + "module": "sidewinder.analysis.transform.transformer", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "Python Debugger: Tests", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": [ + "test/" + ], + "console": "integratedTerminal", + "justMyCode": false + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4b5a294 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda" +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b7c3892..e873b7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,19 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + [project] -name = "chugubandar" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -requires-python = ">=3.10" -dependencies = [] +name = "sidewinder" +version = "0.0.1" +requires-python = ">=3.12" + +[project.optional-dependencies] +dev = [ + "pytest>=8", +] + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] \ No newline at end of file diff --git a/src/analysis/symbolic/lattice/final_string.py b/src/analysis/symbolic/lattice/final_string.py deleted file mode 100644 index 6b7dbe8..0000000 --- a/src/analysis/symbolic/lattice/final_string.py +++ /dev/null @@ -1,4 +0,0 @@ -from symbolic.lattice.placeholder_string import SidewinderPlaceholderString - -class SidewinderFinalString(SidewinderPlaceholderString): - pass \ No newline at end of file diff --git a/src/analysis/symbolic/runtime/values/heap_object.py b/src/analysis/symbolic/runtime/values/heap_object.py deleted file mode 100644 index f7d99e5..0000000 --- a/src/analysis/symbolic/runtime/values/heap_object.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from analysis.symbolic.runtime.values.key import Key -from analysis.symbolic.runtime.values.symbolic_type import SymbolicType -from analysis.symbolic.runtime.values.symbolic_value import SymbolicValue - - -class HeapObject: - id: int - heap_map: dict[Key, SymbolicValue] - type: SymbolicType \ No newline at end of file diff --git a/src/analysis/symbolic/runtime/values/symbolic_value.py b/src/analysis/symbolic/runtime/values/symbolic_value.py deleted file mode 100644 index 0ef1464..0000000 --- a/src/analysis/symbolic/runtime/values/symbolic_value.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from typing import TypeAlias - -from analysis.symbolic.runtime.values.effector import Effector -from analysis.symbolic.runtime.values.heap_object import HeapObject -from analysis.symbolic.runtime.values.primitive import Primitive - - -SymbolicValue: TypeAlias = ( - Primitive - | HeapObject - | Effector -) \ No newline at end of file diff --git a/src/analysis/__init__.py b/src/sidewinder/analysis/__init__.py similarity index 58% rename from src/analysis/__init__.py rename to src/sidewinder/analysis/__init__.py index 9f1d6f3..09d31eb 100644 --- a/src/analysis/__init__.py +++ b/src/sidewinder/analysis/__init__.py @@ -4,8 +4,8 @@ This package provides comprehensive Python code analysis capabilities. """ -from analysis.analyzer import PythonCodeAnalyzer -from analysis.transform.transformer import SidewinderTransformer +from sidewinder.analysis.analyzer import PythonCodeAnalyzer +from sidewinder.analysis.transform.transformer import SidewinderTransformer __all__ = ['PythonCodeAnalyzer', 'SidewinderTransformer'] __version__ = '1.0.0' \ No newline at end of file diff --git a/src/analysis/analyzer.py b/src/sidewinder/analysis/analyzer.py similarity index 99% rename from src/analysis/analyzer.py rename to src/sidewinder/analysis/analyzer.py index 396f248..4a29e5c 100644 --- a/src/analysis/analyzer.py +++ b/src/sidewinder/analysis/analyzer.py @@ -7,7 +7,7 @@ import ast from typing import Dict, List, Any, Optional -from analysis.transform.transformer import SidewinderTransformer +from sidewinder.analysis.transform.transformer import SidewinderTransformer class PythonCodeAnalyzer: diff --git a/src/analysis/symbolic/__init__.py b/src/sidewinder/analysis/symbolic/__init__.py similarity index 100% rename from src/analysis/symbolic/__init__.py rename to src/sidewinder/analysis/symbolic/__init__.py diff --git a/src/analysis/symbolic/assumption.py b/src/sidewinder/analysis/symbolic/assumption.py similarity index 100% rename from src/analysis/symbolic/assumption.py rename to src/sidewinder/analysis/symbolic/assumption.py diff --git a/src/analysis/symbolic/callable_value.py b/src/sidewinder/analysis/symbolic/callable_value.py similarity index 50% rename from src/analysis/symbolic/callable_value.py rename to src/sidewinder/analysis/symbolic/callable_value.py index 94c01c6..151ca53 100644 --- a/src/analysis/symbolic/callable_value.py +++ b/src/sidewinder/analysis/symbolic/callable_value.py @@ -1,4 +1,4 @@ -from symbolic.value import SidewinderSymbolicValue +from sidewinder.analysis.symbolic.value import SidewinderSymbolicValue class SidewinderSymbolicCallableValue(SidewinderSymbolicValue): pass \ No newline at end of file diff --git a/src/analysis/symbolic/effect.py b/src/sidewinder/analysis/symbolic/effect.py similarity index 100% rename from src/analysis/symbolic/effect.py rename to src/sidewinder/analysis/symbolic/effect.py diff --git a/src/analysis/symbolic/guard.py b/src/sidewinder/analysis/symbolic/guard.py similarity index 100% rename from src/analysis/symbolic/guard.py rename to src/sidewinder/analysis/symbolic/guard.py diff --git a/src/analysis/symbolic/hook/__init__.py b/src/sidewinder/analysis/symbolic/hook/__init__.py similarity index 57% rename from src/analysis/symbolic/hook/__init__.py rename to src/sidewinder/analysis/symbolic/hook/__init__.py index 4fc8f74..4956b7f 100644 --- a/src/analysis/symbolic/hook/__init__.py +++ b/src/sidewinder/analysis/symbolic/hook/__init__.py @@ -1,4 +1,4 @@ -from analysis.symbolic.hook.sidewinder_hook import SidewinderHook, SidewinderHookNames +from sidewinder.analysis.symbolic.hook.sidewinder_hook import SidewinderHook, SidewinderHookNames from typing import Dict diff --git a/src/analysis/symbolic/hook/sidewinder_hook.py b/src/sidewinder/analysis/symbolic/hook/sidewinder_hook.py similarity index 95% rename from src/analysis/symbolic/hook/sidewinder_hook.py rename to src/sidewinder/analysis/symbolic/hook/sidewinder_hook.py index 2d842d6..7b1b210 100644 --- a/src/analysis/symbolic/hook/sidewinder_hook.py +++ b/src/sidewinder/analysis/symbolic/hook/sidewinder_hook.py @@ -1,7 +1,7 @@ from enum import Enum, auto from typing import Any, Protocol -from analysis.symbolic.runtime.memory.state import SidewinderState +from sidewinder.analysis.symbolic.runtime.memory.state import SidewinderState class SidewinderHookCallable(Protocol): diff --git a/src/analysis/symbolic/lattice/__init__.py b/src/sidewinder/analysis/symbolic/lattice/__init__.py similarity index 100% rename from src/analysis/symbolic/lattice/__init__.py rename to src/sidewinder/analysis/symbolic/lattice/__init__.py diff --git a/src/sidewinder/analysis/symbolic/lattice/final_string.py b/src/sidewinder/analysis/symbolic/lattice/final_string.py new file mode 100644 index 0000000..cc2d9b5 --- /dev/null +++ b/src/sidewinder/analysis/symbolic/lattice/final_string.py @@ -0,0 +1,4 @@ +from sidewinder.analysis.symbolic.lattice.placeholder_string import SidewinderPlaceholderString + +class SidewinderFinalString(SidewinderPlaceholderString): + pass \ No newline at end of file diff --git a/src/analysis/symbolic/lattice/placeholder_string.py b/src/sidewinder/analysis/symbolic/lattice/placeholder_string.py similarity index 100% rename from src/analysis/symbolic/lattice/placeholder_string.py rename to src/sidewinder/analysis/symbolic/lattice/placeholder_string.py diff --git a/src/analysis/symbolic/runtime/effect/__init__.py b/src/sidewinder/analysis/symbolic/runtime/effect/__init__.py similarity index 100% rename from src/analysis/symbolic/runtime/effect/__init__.py rename to src/sidewinder/analysis/symbolic/runtime/effect/__init__.py diff --git a/src/analysis/symbolic/runtime/effect/effect_type.py b/src/sidewinder/analysis/symbolic/runtime/effect/effect_type.py similarity index 100% rename from src/analysis/symbolic/runtime/effect/effect_type.py rename to src/sidewinder/analysis/symbolic/runtime/effect/effect_type.py diff --git a/src/analysis/symbolic/runtime/effect/external_effect.py b/src/sidewinder/analysis/symbolic/runtime/effect/external_effect.py similarity index 76% rename from src/analysis/symbolic/runtime/effect/external_effect.py rename to src/sidewinder/analysis/symbolic/runtime/effect/external_effect.py index cf47e9f..cdbc186 100644 --- a/src/analysis/symbolic/runtime/effect/external_effect.py +++ b/src/sidewinder/analysis/symbolic/runtime/effect/external_effect.py @@ -1,5 +1,5 @@ from analysis.symbolic.runtime.state.effect.effect_type import EffectType -from analysis.symbolic.runtime.values.symbolic_value import SymbolicValue +from sidewinder.analysis.symbolic.runtime.values.symbolic_value import SymbolicValue from enum import Enum, auto class OperationType(Enum): diff --git a/src/analysis/symbolic/runtime/effect/guarded_effect.py b/src/sidewinder/analysis/symbolic/runtime/effect/guarded_effect.py similarity index 65% rename from src/analysis/symbolic/runtime/effect/guarded_effect.py rename to src/sidewinder/analysis/symbolic/runtime/effect/guarded_effect.py index e45e0f0..8917f34 100644 --- a/src/analysis/symbolic/runtime/effect/guarded_effect.py +++ b/src/sidewinder/analysis/symbolic/runtime/effect/guarded_effect.py @@ -1,7 +1,7 @@ from __future__ import annotations from analysis.symbolic.runtime.state.effect import Effect -from analysis.symbolic.runtime.values.symbolic_value import SymbolicValue +from sidewinder.analysis.symbolic.runtime.values.symbolic_value import SymbolicValue class GuardedEffect: diff --git a/src/analysis/symbolic/runtime/memory/call_stack.py b/src/sidewinder/analysis/symbolic/runtime/memory/call_stack.py similarity index 100% rename from src/analysis/symbolic/runtime/memory/call_stack.py rename to src/sidewinder/analysis/symbolic/runtime/memory/call_stack.py diff --git a/src/analysis/symbolic/runtime/memory/globals.py b/src/sidewinder/analysis/symbolic/runtime/memory/globals.py similarity index 100% rename from src/analysis/symbolic/runtime/memory/globals.py rename to src/sidewinder/analysis/symbolic/runtime/memory/globals.py diff --git a/src/analysis/symbolic/runtime/memory/heap.py b/src/sidewinder/analysis/symbolic/runtime/memory/heap.py similarity index 100% rename from src/analysis/symbolic/runtime/memory/heap.py rename to src/sidewinder/analysis/symbolic/runtime/memory/heap.py diff --git a/src/analysis/symbolic/runtime/memory/state.py b/src/sidewinder/analysis/symbolic/runtime/memory/state.py similarity index 100% rename from src/analysis/symbolic/runtime/memory/state.py rename to src/sidewinder/analysis/symbolic/runtime/memory/state.py diff --git a/src/analysis/symbolic/runtime/values/effector.py b/src/sidewinder/analysis/symbolic/runtime/values/effector.py similarity index 71% rename from src/analysis/symbolic/runtime/values/effector.py rename to src/sidewinder/analysis/symbolic/runtime/values/effector.py index a38bf04..48fcc15 100644 --- a/src/analysis/symbolic/runtime/values/effector.py +++ b/src/sidewinder/analysis/symbolic/runtime/values/effector.py @@ -1,6 +1,6 @@ from typing import Protocol -from analysis.symbolic.runtime.values.symbolic_value import SymbolicValue +from sidewinder.analysis.symbolic.runtime.values.symbolic_value import SymbolicValue from analysis.symbolic.runtime.state.effect.guarded_effect import GuardedEffect diff --git a/src/sidewinder/analysis/symbolic/runtime/values/heap_object.py b/src/sidewinder/analysis/symbolic/runtime/values/heap_object.py new file mode 100644 index 0000000..f778cb8 --- /dev/null +++ b/src/sidewinder/analysis/symbolic/runtime/values/heap_object.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from sidewinder.analysis.symbolic.runtime.values.key import Key +from sidewinder.analysis.symbolic.runtime.values.symbolic_type import SymbolicType +from sidewinder.analysis.symbolic.runtime.values.symbolic_value import SymbolicValue + + +class HeapObject: + id: int + heap_map: dict[Key, SymbolicValue] + type: SymbolicType \ No newline at end of file diff --git a/src/analysis/symbolic/runtime/values/key.py b/src/sidewinder/analysis/symbolic/runtime/values/key.py similarity index 100% rename from src/analysis/symbolic/runtime/values/key.py rename to src/sidewinder/analysis/symbolic/runtime/values/key.py diff --git a/src/analysis/symbolic/runtime/values/primitive.py b/src/sidewinder/analysis/symbolic/runtime/values/primitive.py similarity index 100% rename from src/analysis/symbolic/runtime/values/primitive.py rename to src/sidewinder/analysis/symbolic/runtime/values/primitive.py diff --git a/src/analysis/symbolic/runtime/values/symbolic_type.py b/src/sidewinder/analysis/symbolic/runtime/values/symbolic_type.py similarity index 100% rename from src/analysis/symbolic/runtime/values/symbolic_type.py rename to src/sidewinder/analysis/symbolic/runtime/values/symbolic_type.py diff --git a/src/sidewinder/analysis/symbolic/runtime/values/symbolic_value.py b/src/sidewinder/analysis/symbolic/runtime/values/symbolic_value.py new file mode 100644 index 0000000..78aa771 --- /dev/null +++ b/src/sidewinder/analysis/symbolic/runtime/values/symbolic_value.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import TypeAlias + +from sidewinder.analysis.symbolic.runtime.values.effector import Effector +from sidewinder.analysis.symbolic.runtime.values.heap_object import HeapObject +from sidewinder.analysis.symbolic.runtime.values.primitive import Primitive + + +SymbolicValue: TypeAlias = ( + Primitive + | HeapObject + | Effector +) \ No newline at end of file diff --git a/src/analysis/symbolic/value.py b/src/sidewinder/analysis/symbolic/value.py similarity index 100% rename from src/analysis/symbolic/value.py rename to src/sidewinder/analysis/symbolic/value.py diff --git a/src/analysis/transform/__init__.py b/src/sidewinder/analysis/transform/__init__.py similarity index 100% rename from src/analysis/transform/__init__.py rename to src/sidewinder/analysis/transform/__init__.py diff --git a/src/analysis/transform/errors.py b/src/sidewinder/analysis/transform/errors.py similarity index 100% rename from src/analysis/transform/errors.py rename to src/sidewinder/analysis/transform/errors.py diff --git a/src/analysis/transform/tests.txt b/src/sidewinder/analysis/transform/tests.txt similarity index 100% rename from src/analysis/transform/tests.txt rename to src/sidewinder/analysis/transform/tests.txt diff --git a/src/analysis/transform/transformer.py b/src/sidewinder/analysis/transform/transformer.py similarity index 86% rename from src/analysis/transform/transformer.py rename to src/sidewinder/analysis/transform/transformer.py index fad2eb3..c4d39e8 100644 --- a/src/analysis/transform/transformer.py +++ b/src/sidewinder/analysis/transform/transformer.py @@ -11,22 +11,22 @@ import ast from typing import Dict, List, Any, Union, overload -from analysis.transform.errors import SidewinderIllegalStateError -from analysis.transform.transformer_assign import SidewinderAssignTransformerMixin -from analysis.transform.transformer_base import SidewinderTransformerBase, T -from analysis.transform.transformer_classes import SidewinderClassTransformerMixin -from analysis.transform.transformer_context import TransformerContext -from analysis.transform.transformer_control_flow_breaker import SidewinderControlFlowBreakerTransformerMixin -from analysis.transform.transformer_expr import SidewinderExprTransformerMixin -from analysis.transform.transformer_for import SidewinderForTransformerMixin -from analysis.transform.transformer_functions import SidewinderFunctionTransformerMixin -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers -from analysis.transform.transformer_if import SidewinderIfTransformerMixin -from analysis.transform.transformer_modules import SidewinderModuleTransformerMixin -from analysis.transform.transformer_try import SidewinderTryTransformerMixin -from analysis.transform.transformer_while import SidewinderWhileTransformerMixin -from analysis.transform.transformer_with import SidewinderWithTransformerMixin -from analysis.symbolic.hook import SidewinderHookNames +from sidewinder.analysis.transform.errors import SidewinderIllegalStateError +from sidewinder.analysis.transform.transformer_assign import SidewinderAssignTransformerMixin +from sidewinder.analysis.transform.transformer_base import SidewinderTransformerBase, T +from sidewinder.analysis.transform.transformer_classes import SidewinderClassTransformerMixin +from sidewinder.analysis.transform.transformer_context import TransformerContext +from sidewinder.analysis.transform.transformer_control_flow_breaker import SidewinderControlFlowBreakerTransformerMixin +from sidewinder.analysis.transform.transformer_expr import SidewinderExprTransformerMixin +from sidewinder.analysis.transform.transformer_for import SidewinderForTransformerMixin +from sidewinder.analysis.transform.transformer_functions import SidewinderFunctionTransformerMixin +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.transformer_if import SidewinderIfTransformerMixin +from sidewinder.analysis.transform.transformer_modules import SidewinderModuleTransformerMixin +from sidewinder.analysis.transform.transformer_try import SidewinderTryTransformerMixin +from sidewinder.analysis.transform.transformer_while import SidewinderWhileTransformerMixin +from sidewinder.analysis.transform.transformer_with import SidewinderWithTransformerMixin +from sidewinder.analysis.symbolic.hook import SidewinderHookNames # This is not the transformer which is behind LLMs. This is an AST transformer, which transforms the abstract syntax trees generated by parsing @@ -262,8 +262,10 @@ def transform_code(source_code: str) -> str: # print(i) test_code = """ -def foo(x, *k, **y): - pass +if x: + y = 1 +else: + y = 2 """ # a = [x * 2 for x in range(5)] diff --git a/src/analysis/transform/transformer_assign.py b/src/sidewinder/analysis/transform/transformer_assign.py similarity index 75% rename from src/analysis/transform/transformer_assign.py rename to src/sidewinder/analysis/transform/transformer_assign.py index 25e8d52..c0b09b4 100644 --- a/src/analysis/transform/transformer_assign.py +++ b/src/sidewinder/analysis/transform/transformer_assign.py @@ -1,29 +1,30 @@ import ast import copy -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers class SidewinderAssignTransformerMixin(SidewinderTransformerHelpers): - def visit_Assign(self, node: ast.Assign) -> None: + def visit_Assign(self, node: ast.Assign) -> list[ast.stmt]: """Transform assignment statement - transform value.""" transformed_value = self._visit_expr(node.value) if len(node.targets) != 1: raise NotImplementedError(f"Sidewinder currently only supports a single target for assign statements like {ast.unparse(node)}") + to_return = [] for target in node.targets: - self._visit_target(target, transformed_value) - return None + to_return.extend(self._visit_target(target, transformed_value)) + return to_return - def visit_AnnAssign(self, node: ast.AnnAssign) -> None: + def visit_AnnAssign(self, node: ast.AnnAssign) -> list[ast.stmt]: """Transform annotated assignment statement.""" # Transform the annotation (it's an expr) node.annotation = self._visit_expr(node.annotation) # Transform the value if it exists (can be None) if node.value is not None: - self._visit_target(node.target, self._visit_expr(node.value)) - return None + return self._visit_target(node.target, self._visit_expr(node.value)) + return [] - def visit_AugAssign(self, node: ast.AugAssign) -> None: + def visit_AugAssign(self, node: ast.AugAssign) -> list[ast.stmt]: op_map = { ast.Add: 'add', ast.Sub: 'sub', @@ -64,11 +65,13 @@ def visit_AugAssign(self, node: ast.AugAssign) -> None: ) tmp = self._fresh_temp() - self.current_context.append_stmt( + to_return = [] + to_return.append( ast.Assign( targets=[ast.Name(id=tmp, ctx=ast.Store())], value=rhs_value, lineno=0, col_offset=0, ) ) - self._visit_target(node.target, ast.Name(id=tmp, ctx=ast.Load())) + to_return.extend(self._visit_target(node.target, ast.Name(id=tmp, ctx=ast.Load()))) + return to_return diff --git a/src/analysis/transform/transformer_base.py b/src/sidewinder/analysis/transform/transformer_base.py similarity index 93% rename from src/analysis/transform/transformer_base.py rename to src/sidewinder/analysis/transform/transformer_base.py index b9c7aee..e8a4450 100644 --- a/src/analysis/transform/transformer_base.py +++ b/src/sidewinder/analysis/transform/transformer_base.py @@ -1,7 +1,7 @@ import ast from typing import TypeVar, TypeAlias, overload, Union, Any -from analysis.transform.transformer_context import TransformerContext +from sidewinder.analysis.transform.transformer_context import TransformerContext T = TypeVar('T', bound=ast.expr) diff --git a/src/analysis/transform/transformer_classes.py b/src/sidewinder/analysis/transform/transformer_classes.py similarity index 90% rename from src/analysis/transform/transformer_classes.py rename to src/sidewinder/analysis/transform/transformer_classes.py index b026a4e..3cc7fa7 100644 --- a/src/analysis/transform/transformer_classes.py +++ b/src/sidewinder/analysis/transform/transformer_classes.py @@ -1,7 +1,7 @@ import ast from typing import Any -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers class SidewinderClassTransformerMixin(SidewinderTransformerHelpers): def visit_ClassDef(self, node: ast.ClassDef) -> Any: diff --git a/src/analysis/transform/transformer_context.py b/src/sidewinder/analysis/transform/transformer_context.py similarity index 100% rename from src/analysis/transform/transformer_context.py rename to src/sidewinder/analysis/transform/transformer_context.py diff --git a/src/analysis/transform/transformer_control_flow_breaker.py b/src/sidewinder/analysis/transform/transformer_control_flow_breaker.py similarity index 94% rename from src/analysis/transform/transformer_control_flow_breaker.py rename to src/sidewinder/analysis/transform/transformer_control_flow_breaker.py index 0d2b739..668e9bd 100644 --- a/src/analysis/transform/transformer_control_flow_breaker.py +++ b/src/sidewinder/analysis/transform/transformer_control_flow_breaker.py @@ -1,8 +1,8 @@ import ast from typing import Any, Union -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers -from analysis.symbolic.hook import SidewinderHookNames +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.symbolic.hook import SidewinderHookNames class SidewinderControlFlowBreakerTransformerMixin(SidewinderTransformerHelpers): def visit_Return(self, node: ast.Return) -> Any: diff --git a/src/analysis/transform/transformer_expr.py b/src/sidewinder/analysis/transform/transformer_expr.py similarity index 96% rename from src/analysis/transform/transformer_expr.py rename to src/sidewinder/analysis/transform/transformer_expr.py index 72d8f8e..ecdee21 100644 --- a/src/analysis/transform/transformer_expr.py +++ b/src/sidewinder/analysis/transform/transformer_expr.py @@ -1,9 +1,9 @@ import ast from typing import Any -from analysis.transform.errors import SidewinderIllegalStateError -from analysis.symbolic.hook import SidewinderHookNames -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.errors import SidewinderIllegalStateError +from sidewinder.analysis.symbolic.hook import SidewinderHookNames +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers class SidewinderExprTransformerMixin(SidewinderTransformerHelpers): def visit_BinOp(self, node: ast.BinOp) -> tuple[list[ast.stmt], ast.expr]: @@ -246,7 +246,7 @@ def visit_Subscript(self, node: ast.Subscript) -> tuple[list[ast.stmt], ast.expr def visit_Slice(self, node: ast.Slice) -> Any: """Transform slice - convert to slice object.""" # a[1:5:2] -> slice(1, 5, 2) - return ast.Call( + return [], ast.Call( func=ast.Name(id='slice', ctx=ast.Load()), args=[ self._visit_expr(node.lower) if node.lower else ast.Constant(value=None), @@ -359,7 +359,7 @@ def visit_Lambda(self, node: ast.Lambda) -> Any: # Transform body node.body = self._visit_expr(node.body) - return node + return [], node def visit_IfExp(self, node: ast.IfExp) -> tuple[list[ast.stmt], ast.expr]: result = self._fresh_temp("__sidewinder_ifexp") @@ -403,22 +403,22 @@ def visit_Dict(self, node: ast.Dict) -> Any: assert (k is not None for k in node.keys), "Unpacking dict is not supported yet" node.keys = [self._visit_expr(k) if k else None for k in node.keys] node.values = [self._visit_expr(v) for v in node.values] - return node + return [], node def visit_Set(self, node: ast.Set) -> Any: """Transform set literal.""" node.elts = [self._visit_expr(elt) for elt in node.elts] - return node + return [], node def visit_List(self, node: ast.List) -> Any: """Transform list literal.""" node.elts = [self._visit_expr(elt) for elt in node.elts] - return node + return [], node def visit_Tuple(self, node: ast.Tuple) -> Any: """Transform tuple literal.""" node.elts = [self._visit_expr(elt) for elt in node.elts] - return node + return [], node def visit_Await(self, node: ast.Await) -> Any: """Transform await expression.""" @@ -429,25 +429,25 @@ def visit_FormattedValue(self, node: ast.FormattedValue) -> Any: node.value = self._visit_expr(node.value) if node.format_spec: node.format_spec = self._visit_expr(node.format_spec) - return node + return [], node def visit_JoinedStr(self, node: ast.JoinedStr) -> Any: """Transform f-string.""" node.values = [self._visit_expr(val) for val in node.values] - return node + return [], node def visit_Starred(self, node: ast.Starred) -> Any: """Transform starred expression.""" node.value = self._visit_expr(node.value) - return node + return [], node def visit_Name(self, node: ast.Name) -> Any: """Name nodes are unchanged.""" - return node + return [], node def visit_Constant(self, node: ast.Constant) -> Any: """Constant nodes are unchanged.""" - return node + return [], node def visit_NamedExpr(self, node: ast.NamedExpr) -> Any: """Transform named expression (walrus operator).""" diff --git a/src/analysis/transform/transformer_for.py b/src/sidewinder/analysis/transform/transformer_for.py similarity index 97% rename from src/analysis/transform/transformer_for.py rename to src/sidewinder/analysis/transform/transformer_for.py index 7169a94..bf65200 100644 --- a/src/analysis/transform/transformer_for.py +++ b/src/sidewinder/analysis/transform/transformer_for.py @@ -1,6 +1,6 @@ import ast -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers class SidewinderForTransformerMixin(SidewinderTransformerHelpers): def visit_For(self, node: ast.For) -> list[ast.stmt]: diff --git a/src/analysis/transform/transformer_functions.py b/src/sidewinder/analysis/transform/transformer_functions.py similarity index 96% rename from src/analysis/transform/transformer_functions.py rename to src/sidewinder/analysis/transform/transformer_functions.py index e92eff1..6b38ed1 100644 --- a/src/analysis/transform/transformer_functions.py +++ b/src/sidewinder/analysis/transform/transformer_functions.py @@ -1,7 +1,7 @@ import ast from typing import Any, Union -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers class SidewinderFunctionTransformerMixin(SidewinderTransformerHelpers): # ========== Function (sync and async) Def Nodes ========== diff --git a/src/analysis/transform/transformer_helpers.py b/src/sidewinder/analysis/transform/transformer_helpers.py similarity index 90% rename from src/analysis/transform/transformer_helpers.py rename to src/sidewinder/analysis/transform/transformer_helpers.py index 5f916a7..4690127 100644 --- a/src/analysis/transform/transformer_helpers.py +++ b/src/sidewinder/analysis/transform/transformer_helpers.py @@ -3,10 +3,10 @@ from collections import defaultdict import copy -from analysis.transform.transformer_base import SidewinderTransformerBase, T -from analysis.transform.transformer_context import TransformerContext -from analysis.transform.errors import SidewinderIllegalStateError -from analysis.symbolic.hook import SidewinderHookNames +from sidewinder.analysis.transform.transformer_base import SidewinderTransformerBase, T +from sidewinder.analysis.transform.transformer_context import TransformerContext +from sidewinder.analysis.transform.errors import SidewinderIllegalStateError +from sidewinder.analysis.symbolic.hook import SidewinderHookNames class SidewinderTransformerHelpers(SidewinderTransformerBase): @@ -90,7 +90,7 @@ def _visit_expr(self, expr: T) -> ast.expr: self.current_context.append_stmt(stmt) return final_expr - def _visit_target(self, target: ast.expr | ast.Tuple | ast.List, visited_rhs: ast.expr) -> None: + def _visit_target(self, target: ast.expr | ast.Tuple | ast.List, visited_rhs: ast.expr) -> List[ast.stmt]: """ Emit statements to assign rhs to target. rhs is already a normalized expression (usually a Name node pointing to a tmp). @@ -104,32 +104,34 @@ def _visit_target(self, target: ast.expr | ast.Tuple | ast.List, visited_rhs: as match target: case ast.Name(): # x = rhs — base case, plain assignment, no transformation needed - self.current_context.append_stmt(ast.Assign(targets=[target], value=visited_rhs, lineno=0, col_offset=0)) + return [ast.Assign(targets=[target], value=visited_rhs, lineno=0, col_offset=0)] case ast.Attribute(): # y.attr = rhs → _visit_expr(y).__setattr__("attr", rhs) obj = self._visit_expr(target.value) - self.current_context.append_stmt( + return [ ast.Expr(value=ast.Call( func=ast.Attribute(value=obj, attr="__sidewinder_setattr__", ctx=ast.Load()), args=[ast.Constant(value = target.attr), visited_rhs], keywords=[], ), lineno=0, col_offset=0) - ) + ] case ast.Subscript(): # y[i] = rhs → _visit_expr(y).__setitem__(_visit_expr(i), rhs) obj = self._visit_expr(target.value) idx = self._visit_expr(target.slice) - self.current_context.append_stmt( + return [ ast.Expr(value=ast.Call( func=ast.Attribute(value=obj, attr="__sidewinder_setitem__", ctx=ast.Load()), args=[idx, visited_rhs], keywords=[], ), lineno=0, col_offset=0) - ) + ] case ast.Tuple() | ast.List(): + to_return = [] + starred_indices = [i for i, e in enumerate(target.elts) if isinstance(e, ast.Starred)] @@ -137,7 +139,7 @@ def _visit_target(self, target: ast.expr | ast.Tuple | ast.List, visited_rhs: as # simple case: x, y, z = rhs # emit: _iter = visited_rhs.__iter__() iter_tmp = self._fresh_temp() - self.current_context.append_stmt( + to_return.append( ast.Assign( targets=[ast.Name(id=iter_tmp, ctx=ast.Store())], value=ast.Call( @@ -150,7 +152,7 @@ def _visit_target(self, target: ast.expr | ast.Tuple | ast.List, visited_rhs: as for elt in target.elts: # emit: _tmpN = _iter.__next__() next_tmp = self._fresh_temp() - self.current_context.append_stmt( + to_return.append( ast.Assign( targets=[ast.Name(id=next_tmp, ctx=ast.Store())], value=ast.Call( @@ -164,7 +166,9 @@ def _visit_target(self, target: ast.expr | ast.Tuple | ast.List, visited_rhs: as lineno=0, col_offset=0, ) ) + assert False, "This does not look right, fix when you get to it" self._visit_target(elt, ast.Name(id=next_tmp, ctx=ast.Load())) + return to_return elif len(starred_indices) == 1: raise NotImplementedError("starred unpacking in tuple target") diff --git a/src/analysis/transform/transformer_if.py b/src/sidewinder/analysis/transform/transformer_if.py similarity index 92% rename from src/analysis/transform/transformer_if.py rename to src/sidewinder/analysis/transform/transformer_if.py index 543ff4b..ba0a858 100644 --- a/src/analysis/transform/transformer_if.py +++ b/src/sidewinder/analysis/transform/transformer_if.py @@ -1,7 +1,7 @@ import ast -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers -from analysis.symbolic.hook import SidewinderHookNames +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.symbolic.hook import SidewinderHookNames class SidewinderIfTransformerMixin(SidewinderTransformerHelpers): def visit_If(self, node: ast.If) -> list[ast.stmt]: diff --git a/src/analysis/transform/transformer_modules.py b/src/sidewinder/analysis/transform/transformer_modules.py similarity index 88% rename from src/analysis/transform/transformer_modules.py rename to src/sidewinder/analysis/transform/transformer_modules.py index d9ec3f6..7d46aaf 100644 --- a/src/analysis/transform/transformer_modules.py +++ b/src/sidewinder/analysis/transform/transformer_modules.py @@ -1,7 +1,7 @@ import ast from typing import Any -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers class SidewinderModuleTransformerMixin(SidewinderTransformerHelpers): def visit_Module(self, node: ast.Module) -> Any: diff --git a/src/analysis/transform/transformer_try.py b/src/sidewinder/analysis/transform/transformer_try.py similarity index 96% rename from src/analysis/transform/transformer_try.py rename to src/sidewinder/analysis/transform/transformer_try.py index a943aa8..49fbd65 100644 --- a/src/analysis/transform/transformer_try.py +++ b/src/sidewinder/analysis/transform/transformer_try.py @@ -1,8 +1,8 @@ import ast -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers -from analysis.transform.errors import SidewinderIllegalStateError -from analysis.symbolic.hook import SidewinderHookNames +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.errors import SidewinderIllegalStateError +from sidewinder.analysis.symbolic.hook import SidewinderHookNames from typing import Any diff --git a/src/analysis/transform/transformer_while.py b/src/sidewinder/analysis/transform/transformer_while.py similarity index 95% rename from src/analysis/transform/transformer_while.py rename to src/sidewinder/analysis/transform/transformer_while.py index 5a46e46..3b10a7c 100644 --- a/src/analysis/transform/transformer_while.py +++ b/src/sidewinder/analysis/transform/transformer_while.py @@ -1,7 +1,7 @@ import ast -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers -from analysis.symbolic.hook import SidewinderHookNames +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.symbolic.hook import SidewinderHookNames class SidewinderWhileTransformerMixin(SidewinderTransformerHelpers): def visit_While(self, node: ast.While) -> list[ast.stmt]: diff --git a/src/analysis/transform/transformer_with.py b/src/sidewinder/analysis/transform/transformer_with.py similarity index 97% rename from src/analysis/transform/transformer_with.py rename to src/sidewinder/analysis/transform/transformer_with.py index 834e8fb..0766857 100644 --- a/src/analysis/transform/transformer_with.py +++ b/src/sidewinder/analysis/transform/transformer_with.py @@ -1,6 +1,6 @@ import ast -from analysis.transform.transformer_helpers import SidewinderTransformerHelpers +from sidewinder.analysis.transform.transformer_helpers import SidewinderTransformerHelpers class SidewinderWithTransformerMixin(SidewinderTransformerHelpers): def visit_With(self, node: ast.With) -> list[ast.stmt]: diff --git a/src/analysis/transformer/rules.py b/src/sidewinder/analysis/transformer/rules.py similarity index 100% rename from src/analysis/transformer/rules.py rename to src/sidewinder/analysis/transformer/rules.py diff --git a/src/analysis/transformer/version.py b/src/sidewinder/analysis/transformer/version.py similarity index 100% rename from src/analysis/transformer/version.py rename to src/sidewinder/analysis/transformer/version.py diff --git a/src/driver.py b/src/sidewinder/driver.py similarity index 94% rename from src/driver.py rename to src/sidewinder/driver.py index cfdaa84..e86b747 100644 --- a/src/driver.py +++ b/src/sidewinder/driver.py @@ -1,7 +1,7 @@ import sys import os from pathlib import Path -from analysis.analyzer import PythonCodeAnalyzer +from sidewinder.analysis.analyzer import PythonCodeAnalyzer def main(): diff --git a/test/analysis/transform/inputs/if/if_elif.py b/test/analysis/transform/inputs/if/if_elif.py new file mode 100644 index 0000000..73885e6 --- /dev/null +++ b/test/analysis/transform/inputs/if/if_elif.py @@ -0,0 +1,6 @@ +if x: + y1 +elif z: + y2 +else: + y3 \ No newline at end of file diff --git a/test/analysis/transform/inputs/if/if_elif_no_else.py b/test/analysis/transform/inputs/if/if_elif_no_else.py new file mode 100644 index 0000000..4bfe337 --- /dev/null +++ b/test/analysis/transform/inputs/if/if_elif_no_else.py @@ -0,0 +1,4 @@ +if x: + y1 +elif z: + y2 \ No newline at end of file diff --git a/test/analysis/transform/inputs/if/if_multiple_elif.py b/test/analysis/transform/inputs/if/if_multiple_elif.py new file mode 100644 index 0000000..557cfaf --- /dev/null +++ b/test/analysis/transform/inputs/if/if_multiple_elif.py @@ -0,0 +1,8 @@ +if x: + y1 +elif z: + y2 +elif p: + y3 +else: + y4 \ No newline at end of file diff --git a/test/analysis/transform/inputs/if/if_nested_else.py b/test/analysis/transform/inputs/if/if_nested_else.py new file mode 100644 index 0000000..da0f722 --- /dev/null +++ b/test/analysis/transform/inputs/if/if_nested_else.py @@ -0,0 +1,7 @@ +if x: + y1 +else: + if z: + y2 + else: + y3 \ No newline at end of file diff --git a/test/analysis/transform/inputs/if/if_nested_if.py b/test/analysis/transform/inputs/if/if_nested_if.py new file mode 100644 index 0000000..4ee6a8b --- /dev/null +++ b/test/analysis/transform/inputs/if/if_nested_if.py @@ -0,0 +1,7 @@ +if x: + if z: + y1 + else: + y3 +else: + y2 \ No newline at end of file diff --git a/test/analysis/transform/inputs/if/if_simple.py b/test/analysis/transform/inputs/if/if_simple.py new file mode 100644 index 0000000..0be57fd --- /dev/null +++ b/test/analysis/transform/inputs/if/if_simple.py @@ -0,0 +1,4 @@ +if x: + y1 +else: + y2 \ No newline at end of file diff --git a/test/analysis/transform/inputs/if/if_without_else.py b/test/analysis/transform/inputs/if/if_without_else.py new file mode 100644 index 0000000..249d9b3 --- /dev/null +++ b/test/analysis/transform/inputs/if/if_without_else.py @@ -0,0 +1,2 @@ +if x: + y1 \ No newline at end of file diff --git a/test/analysis/transform/outputs/if/if_elif_expected.py b/test/analysis/transform/outputs/if/if_elif_expected.py new file mode 100644 index 0000000..c60151d --- /dev/null +++ b/test/analysis/transform/outputs/if/if_elif_expected.py @@ -0,0 +1,15 @@ +from sidewinder.analysis.symbolic.state import SidewinderState + +__sidewinder_cond0 = x +__sidewinder_condition_true__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y1 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +__sidewinder_cond1 = z +__sidewinder_condition_true__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y2 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y3 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) \ No newline at end of file diff --git a/test/analysis/transform/outputs/if/if_elif_no_else_expected.py b/test/analysis/transform/outputs/if/if_elif_no_else_expected.py new file mode 100644 index 0000000..e44afe0 --- /dev/null +++ b/test/analysis/transform/outputs/if/if_elif_no_else_expected.py @@ -0,0 +1,12 @@ +from sidewinder.analysis.symbolic.state import SidewinderState + +__sidewinder_cond0 = x +__sidewinder_condition_true__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y1 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +__sidewinder_cond1 = z +__sidewinder_condition_true__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y2 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) \ No newline at end of file diff --git a/test/analysis/transform/outputs/if/if_multiple_elif_expected.py b/test/analysis/transform/outputs/if/if_multiple_elif_expected.py new file mode 100644 index 0000000..380df62 --- /dev/null +++ b/test/analysis/transform/outputs/if/if_multiple_elif_expected.py @@ -0,0 +1,22 @@ +from sidewinder.analysis.symbolic.state import SidewinderState + +__sidewinder_cond0 = x +__sidewinder_condition_true__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y1 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +__sidewinder_cond1 = z +__sidewinder_condition_true__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y2 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) + +__sidewinder_cond2 = p +__sidewinder_condition_true__(__sidewinder_cond2, __sidewinder_state=__sidewinder_state) +y3 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond2, __sidewinder_state=__sidewinder_state) +y4 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) \ No newline at end of file diff --git a/test/analysis/transform/outputs/if/if_nested_else_expected.py b/test/analysis/transform/outputs/if/if_nested_else_expected.py new file mode 100644 index 0000000..2c2a7cd --- /dev/null +++ b/test/analysis/transform/outputs/if/if_nested_else_expected.py @@ -0,0 +1,17 @@ +from sidewinder.analysis.symbolic.state import SidewinderState + +__sidewinder_cond0 = x +__sidewinder_condition_true__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y1 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) + +__sidewinder_cond1 = z +__sidewinder_condition_true__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y2 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y3 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) + +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) \ No newline at end of file diff --git a/test/analysis/transform/outputs/if/if_nested_if_expected.py b/test/analysis/transform/outputs/if/if_nested_if_expected.py new file mode 100644 index 0000000..11b415a --- /dev/null +++ b/test/analysis/transform/outputs/if/if_nested_if_expected.py @@ -0,0 +1,16 @@ +from sidewinder.analysis.symbolic.state import SidewinderState + +__sidewinder_cond0 = x +__sidewinder_condition_true__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) + +__sidewinder_cond1 = z +__sidewinder_condition_true__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y1 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond1, __sidewinder_state=__sidewinder_state) +y3 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y2 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) \ No newline at end of file diff --git a/test/analysis/transform/outputs/if/if_simple_expected.py b/test/analysis/transform/outputs/if/if_simple_expected.py new file mode 100644 index 0000000..b990c84 --- /dev/null +++ b/test/analysis/transform/outputs/if/if_simple_expected.py @@ -0,0 +1,9 @@ +from sidewinder.analysis.symbolic.state import SidewinderState + +__sidewinder_cond0 = x +__sidewinder_condition_true__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y1 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) +__sidewinder_condition_false__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y2 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) \ No newline at end of file diff --git a/test/analysis/transform/outputs/if/if_without_else_expected.py b/test/analysis/transform/outputs/if/if_without_else_expected.py new file mode 100644 index 0000000..19322cc --- /dev/null +++ b/test/analysis/transform/outputs/if/if_without_else_expected.py @@ -0,0 +1,6 @@ +from sidewinder.analysis.symbolic.state import SidewinderState + +__sidewinder_cond0 = x +__sidewinder_condition_true__(__sidewinder_cond0, __sidewinder_state=__sidewinder_state) +y1 +__sidewinder_pop_condition__(__sidewinder_state=__sidewinder_state) \ No newline at end of file diff --git a/test/analysis/transform/test_transform.py b/test/analysis/transform/test_transform.py new file mode 100644 index 0000000..0a7df60 --- /dev/null +++ b/test/analysis/transform/test_transform.py @@ -0,0 +1,88 @@ +# testDriver.py +import ast +import difflib +import pytest +import warnings +from pathlib import Path +from sidewinder.analysis.transform.transformer import SidewinderTransformer # adjust import path + +INPUTS_DIR = Path(__file__).parent / "inputs" +OUTPUTS_DIR = Path(__file__).parent / "outputs" + + +def discover_tests() -> list[tuple[Path, Path]]: + """ + Discover input/output test pairs. + Flags: + - inputs with no matching expected output + - expected outputs with no matching input + """ + input_files = set(INPUTS_DIR.glob("**/*.py")) + output_files = set(OUTPUTS_DIR.glob("**/*_expected.py")) + + # map stem -> path for inputs + input_map = {f.stem: f for f in input_files} + # map stem (without -expected) -> path for outputs + output_map = {f.stem.removesuffix("_expected"): f for f in output_files} + + # flag unmatched inputs + for stem, path in input_map.items(): + if stem not in output_map: + warnings.warn(f"Input file {path.name} has no matching expected output — skipping") + + # flag unmatched outputs + for stem, path in output_map.items(): + if stem not in input_map: + warnings.warn(f"Expected output {path.name} has no matching input — skipping") + + # return matched pairs only + return [ + (input_map[stem], output_map[stem]) + for stem in input_map + if stem in output_map + ] + + +def _parse_file(path: Path) -> ast.Module: + return ast.parse(path.read_text()) + + +def _diff(expected: ast.AST, actual: ast.AST) -> str: + expected_src = ast.unparse(expected) + actual_src = ast.unparse(actual) + diff = difflib.unified_diff( + expected_src.splitlines(keepends=True), + actual_src.splitlines(keepends=True), + fromfile="expected", + tofile="actual", + ) + return "".join(diff) + +test_cases = discover_tests() + +@pytest.mark.parametrize( + "input_file,expected_file", + test_cases, + ids=[f"{input_file.parent.stem}::{input_file.stem}" for input_file, _ in test_cases] +) +def test_transformer(input_file: Path, expected_file: Path) -> None: + # parse input + input_tree = _parse_file(input_file) + + # transform + transformer = SidewinderTransformer() + actual_tree = transformer.visit(input_tree) + ast.fix_missing_locations(actual_tree) + + # parse expected output + expected_tree = _parse_file(expected_file) + + # compare structurally + if ast.dump(expected_tree) != ast.dump(actual_tree): + diff = _diff(expected_tree, actual_tree) + pytest.fail( + f"\nTransformer output does not match expected for {input_file.name}\n" + f"\n--- DIFF ---\n{diff}\n" + f"\n--- EXPECTED ---\n{ast.unparse(expected_tree)}\n" + f"\n--- ACTUAL ---\n{ast.unparse(actual_tree)}\n" + ) \ No newline at end of file