diff --git a/poetry.lock b/poetry.lock index e2626c7..469a47c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -255,6 +255,24 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", toml = ["tomli (>=1.1.0)"] yaml = ["PyYAML"] +[[package]] +name = "beartype" +version = "0.18.5" +description = "Unbearably fast runtime type checking in pure Python." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "beartype-0.18.5-py3-none-any.whl", hash = "sha256:5301a14f2a9a5540fe47ec6d34d758e9cd8331d36c4760fc7a5499ab86310089"}, + {file = "beartype-0.18.5.tar.gz", hash = "sha256:264ddc2f1da9ec94ff639141fbe33d22e12a9f75aa863b83b7046ffff1381927"}, +] + +[package.extras] +all = ["typing-extensions (>=3.10.0.0)"] +dev = ["autoapi (>=0.9.0)", "coverage (>=5.5)", "equinox", "mypy (>=0.800)", "numpy", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pytest (>=4.0.0)", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)"] +doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"] +test-tox = ["equinox", "mypy (>=0.800)", "numpy", "pandera", "pytest (>=4.0.0)", "sphinx", "typing-extensions (>=3.10.0.0)"] +test-tox-coverage = ["coverage (>=5.5)"] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -2337,6 +2355,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "plum-dispatch" +version = "2.5.2" +description = "Multiple dispatch in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "plum_dispatch-2.5.2-py3-none-any.whl", hash = "sha256:49f1e1487028849451454c59e330f800aac6ec520a432a0ed3ea6e04b74e2e31"}, + {file = "plum_dispatch-2.5.2.tar.gz", hash = "sha256:1abfccc5a9c751f20dcdb1020c645968dfbc1c33ad3a9a47780834ec332cfe9e"}, +] + +[package.dependencies] +beartype = ">=0.16.2" +rich = ">=10.0" +typing-extensions = ">=4.9.0" + +[package.extras] +dev = ["black (==23.9.0)", "build", "coveralls", "ghp-import", "ipython", "jupyter-book", "mypy", "numpy", "pre-commit", "pyright (>=1.1.331)", "pytest (>=6)", "pytest-cov", "ruff (==0.1.0)", "sybil", "tox", "wheel"] + [[package]] name = "pre-commit" version = "3.5.0" @@ -3695,4 +3732,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4" -content-hash = "3e4d318df9f3432f54a9bcbf6c89c0d90842bc2bfd69a04e38574975b446742f" +content-hash = "35e3f96ffe77f066e05a99a5652341dc128bdd33c023aaf5e3c466edb1c15426" diff --git a/pyproject.toml b/pyproject.toml index 9327259..c8683e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ typing-extensions = { version = ">=4", python = "<3.9" } graphviz = ">=0.20.1" asciinet = ">=0.3.1" msgpack = ">=1" +plum-dispatch = ">=2.0" [tool.poetry.group.dev.dependencies] pytest = ">=7.3.2" @@ -79,7 +80,12 @@ exclude = [ fix = true [tool.ruff.lint] -ignore = ["PLR0913"] +ignore = [ + "F811", + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", +] select = [ "E", # pycodestyle "F", # pyflakes diff --git a/src/astx/base.py b/src/astx/base.py index e6192f7..ca216a7 100644 --- a/src/astx/base.py +++ b/src/astx/base.py @@ -286,7 +286,7 @@ class Undefined(Expr): def get_struct(self, simplified: bool = False) -> ReprStruct: """Return a simple structure that represents the object.""" value = "UNDEFINED" - key = "DATA-TYPE" + key = "UNDEFINED" return self._prepare_struct(key, value, simplified) @@ -316,7 +316,7 @@ def __str__(self) -> str: def get_struct(self, simplified: bool = False) -> ReprStruct: """Return a simple structure that represents the object.""" - key = "DATA-TYPE" + key = f"DATA-TYPE[{self.__class__.__name__}]" value = self.name return self._prepare_struct(key, value, simplified) diff --git a/src/astx/transpilers/__init__.py b/src/astx/transpilers/__init__.py new file mode 100644 index 0000000..5d89ec9 --- /dev/null +++ b/src/astx/transpilers/__init__.py @@ -0,0 +1 @@ +"""ASTx Transpilers.""" diff --git a/src/astx/transpilers/python.py b/src/astx/transpilers/python.py new file mode 100644 index 0000000..2f586a3 --- /dev/null +++ b/src/astx/transpilers/python.py @@ -0,0 +1,115 @@ +"""ASTx Python transpiler.""" + +from typing import Type + +from plum import dispatch + +import astx + + +class ASTxPythonTranspiler: + """ + Transpiler that converts ASTx nodes to Python code. + + Notes + ----- + Please keep the visit method in alphabet order according to the node type. + The visit method for astx.AST should be the first one. + """ + + def __init__(self) -> None: + self.indent_level = 0 + self.indent_str = " " # 4 spaces + + def _generate_block(self, block: astx.Block) -> str: + """Generate code for a block of statements with proper indentation.""" + self.indent_level += 1 + indent = self.indent_str * self.indent_level + lines = [indent + self.visit(node) for node in block.nodes] + result = ( + "\n".join(lines) + if lines + else self.indent_str * self.indent_level + "pass" + ) + self.indent_level -= 1 + return result + + @dispatch.abstract + def visit(self, expr: astx.AST) -> str: + """Translate an ASTx expression.""" + raise Exception(f"Not implemented yet ({expr}).") + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.Argument) -> str: + """Handle UnaryOp nodes.""" + type_ = self.visit(node.type_) + return f"{node.name}: {type_}" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.Arguments) -> str: + """Handle UnaryOp nodes.""" + return ", ".join([self.visit(arg) for arg in node.nodes]) + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.BinaryOp) -> str: + """Handle BinaryOp nodes.""" + lhs = self.visit(node.lhs) + rhs = self.visit(node.rhs) + return f"({lhs} {node.op_code} {rhs})" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.Block) -> str: + """Handle Block nodes.""" + return self._generate_block(node) + + @dispatch # type: ignore[no-redef] + def visit(self, node: Type[astx.Int32]) -> str: + """Handle Int32 nodes.""" + return "int" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.LiteralBoolean) -> str: + """Handle LiteralBoolean nodes.""" + return "True" if node.value else "False" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.LiteralInt32) -> str: + """Handle LiteralInt32 nodes.""" + return str(node.value) + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.Function) -> str: + """Handle Function nodes.""" + args = self.visit(node.prototype.args) + returns = ( + f" -> {self.visit(node.prototype.return_type)}" + if node.prototype.return_type + else "" + ) + header = f"def {node.name}({args}){returns}:" + body = self.visit(node.body) + return f"{header}\n{body}" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.FunctionReturn) -> str: + """Handle FunctionReturn nodes.""" + value = self.visit(node.value) if node.value else "" + return f"return {value}" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.UnaryOp) -> str: + """Handle UnaryOp nodes.""" + operand = self.visit(node.operand) + return f"({node.op_code}{operand})" + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.Variable) -> str: + """Handle Variable nodes.""" + return node.name + + @dispatch # type: ignore[no-redef] + def visit(self, node: astx.VariableAssignment) -> str: + """Handle VariableAssignment nodes.""" + target = node.name + value = self.visit(node.value) + return f"{target} = {value}" diff --git a/src/astx/viz.py b/src/astx/viz.py index e988ae9..7f4f878 100644 --- a/src/astx/viz.py +++ b/src/astx/viz.py @@ -365,7 +365,7 @@ def graph_to_ascii(graph: Digraph, timeout: int = 10) -> str: ) result = _asciigraph.graph_to_ascii(graph, timeout=timeout) - return cast(str, result) + return f"\n{result}\n" _asciigraph.graph_to_ascii = types.MethodType( diff --git a/tests/transpilers/__init__.py b/tests/transpilers/__init__.py new file mode 100644 index 0000000..096afdc --- /dev/null +++ b/tests/transpilers/__init__.py @@ -0,0 +1 @@ +"""Set of tests for transpilers.""" diff --git a/tests/transpilers/test_python.py b/tests/transpilers/test_python.py new file mode 100644 index 0000000..101a75a --- /dev/null +++ b/tests/transpilers/test_python.py @@ -0,0 +1,61 @@ +"""Test Python Transpiler.""" + +import astx + +from astx.transpilers import python as astx2py + + +def test_function() -> None: + """Test astx.Function.""" + # Function parameters + args = astx.Arguments( + astx.Argument(name="x", type_=astx.Int32), + astx.Argument(name="y", type_=astx.Int32), + ) + + # Function body + body = astx.Block() + body.append( + astx.VariableAssignment( + name="result", + value=astx.BinaryOp( + op_code="+", + lhs=astx.Variable(name="x"), + rhs=astx.Variable(name="y"), + loc=astx.SourceLocation(line=2, col=8), + ), + loc=astx.SourceLocation(line=2, col=4), + ) + ) + body.append( + astx.FunctionReturn( + value=astx.Variable(name="result"), + loc=astx.SourceLocation(line=3, col=4), + ) + ) + + # Function definition + add_function = astx.Function( + prototype=astx.FunctionPrototype( + name="add", + args=args, + return_type=astx.Int32, + ), + body=body, + loc=astx.SourceLocation(line=1, col=0), + ) + + # Initialize the generator + generator = astx2py.ASTxPythonTranspiler() + + # Generate Python code + generated_code = generator.visit(add_function) + expected_code = "\n".join( + [ + "def add(x: int, y: int) -> int:", + " result = (x + y)", + " return result", + ] + ) + + assert generated_code == expected_code, "generated_code != expected_code"