Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ASTx Python transpiler #115

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/astx/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/astx/transpilers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""ASTx Transpilers."""
115 changes: 115 additions & 0 deletions src/astx/transpilers/python.py
Original file line number Diff line number Diff line change
@@ -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}"
2 changes: 1 addition & 1 deletion src/astx/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions tests/transpilers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Set of tests for transpilers."""
61 changes: 61 additions & 0 deletions tests/transpilers/test_python.py
Original file line number Diff line number Diff line change
@@ -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"
Loading