Compare commits

...
5 Commits
Author SHA1 Message Date
HEL 4d23e8840e feat(parser): adapt AST printer with new nodes 2026-05-25 22:06:18 +02:00
HEL c64d626d1c refactor(parser): remove inheritance from NodeVisitor
remove the parent NodeVisitor class from PythonParser and implement all custom recursive methods instead
2026-05-25 21:42:04 +02:00
HEL ecab1b74a4 feat(parser): add Python AST nodes 2026-05-25 21:39:20 +02:00
HEL 0bbdf04621 feat(parser): generate python AST classes
use the generation script to create Python AST node classes, also distinguish between Midas type annotation nodes and statements
2026-05-25 20:53:36 +02:00
HEL 939e5af4ce refactor(parser): improve AST class generator
make the generation script more flexible
2026-05-25 20:38:38 +02:00
7 changed files with 644 additions and 180 deletions

No files matched your search

+78 -68
View File
@@ -3,58 +3,34 @@ import re
HEADER = '''""" HEADER = '''"""
This file was generated by a script. Any manual changes might be overwritten. This file was generated by a script. Any manual changes might be overwritten.
Please modify gen/ast.py instead and run gen/gen.py Please modify {defs_path} instead and run {gen_path}
"""''' """'''
SECTION_TEMPLATE = """{banner}
@dataclass(frozen=True, kw_only=True)
class {base}(ABC):
location: Optional[Location] = None
@abstractmethod
def accept(self, visitor: Visitor[T]) -> T: ...
class Visitor(ABC, Generic[T]):
{visitor_methods}
{classes}"""
TEMPLATE = """{header} TEMPLATE = """{header}
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod {imports}
from dataclasses import dataclass
from typing import Any, Generic, Optional, TypeVar
from midas.ast.location import Location
from midas.lexer.token import Token
T = TypeVar("T") T = TypeVar("T")
############## {sections}
# Statements #
##############
@dataclass(frozen=True, kw_only=True)
class Stmt(ABC):
location: Optional[Location] = None
@abstractmethod
def accept(self, visitor: Visitor[T]) -> T: ...
class Visitor(ABC, Generic[T]):
{stmt_visitor_methods}
{statements}
###############
# Expressions #
###############
@dataclass(frozen=True, kw_only=True)
class Expr(ABC):
location: Optional[Location] = None
@abstractmethod
def accept(self, visitor: Visitor[T]) -> T: ...
class Visitor(ABC, Generic[T]):
{expr_visitor_methods}
{expressions}
""" """
VISITOR_METHOD_TEMPLATE = """ VISITOR_METHOD_TEMPLATE = """
@@ -71,6 +47,16 @@ class {cls}({base}):
return visitor.visit_{func_name}(self) return visitor.visit_{func_name}(self)
""" """
SECTION_REGEX = re.compile(
r"^###>\s*(?P<base>[^\n]*?)\s*\|\s*(?P<name>[^\n]*?)(\s*\|\s*(?P<param>[^\n]*?))?\s*?\n(?P<body>.*?)\n###<$",
re.MULTILINE | re.DOTALL,
)
IMPORTS_REGEX = re.compile(
r"^###>\s*Imports\s*?\n(?P<body>.*?)\n###<$",
re.MULTILINE | re.DOTALL,
)
def snake_case(text: str) -> str: def snake_case(text: str) -> str:
return re.sub(r"[A-Z]", lambda c: "_" + c.group().lower(), text).lower().strip("_") return re.sub(r"[A-Z]", lambda c: "_" + c.group().lower(), text).lower().strip("_")
@@ -95,41 +81,65 @@ def make_class(name: str, cls: str, base: str):
return cls_def.strip("\n") return cls_def.strip("\n")
def generate(src: str): def make_banner(text: str) -> str:
classes: list[str] = src.split("\n\n") middle: str = f"# {text} #"
stmt_visitor_methods: list[str] = [] rule: str = "#" * len(middle)
expr_visitor_methods: list[str] = [] return "\n".join((rule, middle, rule))
statements: list[str] = []
expressions: list[str] = []
for cls in classes:
def make_section(full_name: str, base: str, param: str, body: str) -> str:
visitor_methods: list[str] = []
classes: list[str] = []
definitions: list[str] = body.strip("\n").split("\n\n\n")
for cls in definitions:
cls = cls.strip("\n") cls = cls.strip("\n")
name: str = re.match("class (.*?):", cls).group(1) # type: ignore name: str = re.match("class (.*?):", cls).group(1) # type: ignore
print(f"Processing {name}") print(f"Processing {name}")
if name.endswith("Stmt"): visitor_methods.append(make_visitor_method(name, param))
stmt_visitor_methods.append(make_visitor_method(name, "stmt")) classes.append(make_class(name, cls, base))
statements.append(make_class(name, cls, "Stmt"))
elif name.endswith("Expr"):
expr_visitor_methods.append(make_visitor_method(name, "expr"))
expressions.append(make_class(name, cls, "Expr"))
return TEMPLATE.format( return SECTION_TEMPLATE.format(
header=HEADER, banner=make_banner(full_name),
stmt_visitor_methods="\n\n".join(stmt_visitor_methods), base=base,
expr_visitor_methods="\n\n".join(expr_visitor_methods), visitor_methods="\n\n".join(visitor_methods),
statements="\n\n\n".join(statements), classes="\n\n\n".join(classes),
expressions="\n\n\n".join(expressions),
) )
def generate(definitions_path: Path, out_path: Path):
root_dir: Path = Path(__file__).parent.parent
rel_path: Path = definitions_path.relative_to(root_dir)
src: str = definitions_path.read_text()
sections: list[str] = []
imports: str = ""
if m := IMPORTS_REGEX.search(src):
imports = m.group("body").strip("\n")
for section_m in SECTION_REGEX.finditer(src):
full_name: str = section_m.group("name")
base: str = section_m.group("base")
param: str = section_m.group("param") or base.lower()
body: str = section_m.group("body")
sections.append(make_section(full_name, base, param, body))
result: str = TEMPLATE.format(
header=HEADER.format(
defs_path=rel_path,
gen_path=Path(__file__).relative_to(root_dir),
),
imports=imports,
sections="\n\n\n".join(sections),
)
out_path.write_text(result)
def main(): def main():
root: Path = Path(__file__).parent.parent root: Path = Path(__file__).parent.parent
in_path: Path = root / "gen" / "ast.py" defs_dir: Path = root / "gen"
out_path: Path = root / "midas" / "ast" / "midas.py" ast_dir: Path = root / "midas" / "ast"
generate(defs_dir / "midas.py", ast_dir / "midas.py")
src: str = in_path.read_text() generate(defs_dir / "python.py", ast_dir / "python.py")
generated: str = generate(src)
out_path.write_text(generated)
if __name__ == "__main__": if __name__ == "__main__":
+79 -41
View File
@@ -1,72 +1,110 @@
# type: ignore
# ruff: disable[F821, F401]
###> Imports
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Generic, Optional, TypeVar
from midas.ast.location import Location
from midas.lexer.token import Token
###<
###> Stmt | Statements
class SimpleTypeStmt: class SimpleTypeStmt:
name: Token name: Token
template: Optional[TemplateExpr] template: Optional[TemplateExpr]
base: TypeExpr base: TypeExpr
constraint: Optional[Expr] constraint: Optional[Expr]
class SimpleTypeExpr:
name: Token
optional: bool
class LogicalExpr:
left: Expr
operator: Token
right: Expr
class BinaryExpr:
left: Expr
operator: Token
right: Expr
class UnaryExpr:
operator: Token
right: Expr
class GetExpr:
expr: Expr
name: Token
class VariableExpr:
name: Token
class GroupingExpr:
expr: Expr
class LiteralExpr:
value: Any
class WildcardExpr:
token: Token
class TemplateExpr:
type: TypeExpr
class TypeExpr:
name: Token
template: Optional[TemplateExpr]
optional: bool
class ComplexTypeStmt: class ComplexTypeStmt:
name: Token name: Token
template: Optional[TemplateExpr] template: Optional[TemplateExpr]
properties: list[PropertyStmt] properties: list[PropertyStmt]
class PropertyStmt: class PropertyStmt:
name: Token name: Token
type: TypeExpr type: TypeExpr
constraint: Optional[Expr] constraint: Optional[Expr]
class ExtendStmt: class ExtendStmt:
type: TypeExpr type: TypeExpr
operations: list[OpStmt] operations: list[OpStmt]
class OpStmt: class OpStmt:
name: Token name: Token
operand: TypeExpr operand: TypeExpr
result: TypeExpr result: TypeExpr
class PredicateStmt: class PredicateStmt:
name: Token name: Token
subject: Token subject: Token
type: TypeExpr type: TypeExpr
condition: Expr condition: Expr
###<
###> Expr | Expressions
class SimpleTypeExpr:
name: Token
optional: bool
class LogicalExpr:
left: Expr
operator: Token
right: Expr
class BinaryExpr:
left: Expr
operator: Token
right: Expr
class UnaryExpr:
operator: Token
right: Expr
class GetExpr:
expr: Expr
name: Token
class VariableExpr:
name: Token
class GroupingExpr:
expr: Expr
class LiteralExpr:
value: Any
class WildcardExpr:
token: Token
class TemplateExpr:
type: TypeExpr
class TypeExpr:
name: Token
template: Optional[TemplateExpr]
optional: bool
###<
+112
View File
@@ -0,0 +1,112 @@
# type: ignore
# ruff: disable[F821, F401]
###> Imports
import ast
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Generic, Optional, TypeVar
from midas.ast.location import Location
###<
###> MidasType | Type annotations | node
class BaseType:
base: str
param: Optional[MidasType]
class ConstraintType:
type: MidasType
constraint: ast.expr
class FrameColumn:
name: Optional[str]
type: Optional[MidasType]
class FrameType:
columns: list[FrameColumn]
###<
###> Stmt | Statements
class ExpressionStmt:
expr: Expr
class Function:
name: str
posonlyargs: list[Argument]
args: list[Argument]
kwonlyargs: list[Argument]
returns: Optional[MidasType]
@dataclass(frozen=True, kw_only=True)
class Argument:
location: Optional[Location] = None
name: Optional[str]
type: Optional[MidasType]
class TypeAssign:
name: str
type: MidasType
###<
###> Expr | Expressions
class AssignExpr:
name: str
value: Expr
class BinaryExpr:
left: Expr
operator: ast.operator
right: Expr
class UnaryExpr:
operator: ast.unaryop
right: Expr
class CallExpr:
callee: Expr
arguments: list[Expr]
class GetExpr:
object: Expr
name: str
class LiteralExpr:
value: Any
class VariableExpr:
name: str
class LogicalExpr:
left: Expr
operator: ast.boolop
right: Expr
class SetExpr:
object: Expr
name: str
value: Expr
###<
+1 -1
View File
@@ -1,6 +1,6 @@
""" """
This file was generated by a script. Any manual changes might be overwritten. This file was generated by a script. Any manual changes might be overwritten.
Please modify gen/ast.py instead and run gen/gen.py Please modify gen/midas.py instead and run gen/gen.py
""" """
from __future__ import annotations from __future__ import annotations
+120 -17
View File
@@ -350,7 +350,12 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]):
return f"{expr.name.lexeme}{template}{'?' if expr.optional else ''}" return f"{expr.name.lexeme}{template}{'?' if expr.optional else ''}"
class PythonAstPrinter(AstPrinter, p.Expr.Visitor[None]): class PythonAstPrinter(
AstPrinter,
p.MidasType.Visitor[None],
p.Stmt.Visitor[None],
p.Expr.Visitor[None],
):
def visit_base_type(self, node: p.BaseType) -> None: def visit_base_type(self, node: p.BaseType) -> None:
self._write_line("BaseType") self._write_line("BaseType")
with self._child_level(): with self._child_level():
@@ -382,39 +387,137 @@ class PythonAstPrinter(AstPrinter, p.Expr.Visitor[None]):
self._mark_last() self._mark_last()
col.accept(self) col.accept(self)
def visit_function(self, node: p.Function) -> None: def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None:
stmt.expr.accept(self)
def visit_function(self, stmt: p.Function) -> None:
self._write_line("Function") self._write_line("Function")
with self._child_level(): with self._child_level():
self._write_line(f"name: {node.name}") self._write_line(f"name: {stmt.name}")
self._write_line("posonlyargs") self._write_line("posonlyargs")
with self._child_level(): with self._child_level():
for i, arg in enumerate(node.posonlyargs): for i, arg in enumerate(stmt.posonlyargs):
self._idx = i self._idx = i
if i == len(node.posonlyargs) - 1: if i == len(stmt.posonlyargs) - 1:
self._mark_last() self._mark_last()
arg.accept(self) self._print_argument(arg)
self._write_line("args") self._write_line("args")
with self._child_level(): with self._child_level():
for i, arg in enumerate(node.args): for i, arg in enumerate(stmt.args):
self._idx = i self._idx = i
if i == len(node.args) - 1: if i == len(stmt.args) - 1:
self._mark_last() self._mark_last()
arg.accept(self) self._print_argument(arg)
self._write_line("kwonlyargs") self._write_line("kwonlyargs")
with self._child_level(): with self._child_level():
for i, arg in enumerate(node.kwonlyargs): for i, arg in enumerate(stmt.kwonlyargs):
self._idx = i self._idx = i
if i == len(node.kwonlyargs) - 1: if i == len(stmt.kwonlyargs) - 1:
self._mark_last()
self._print_argument(arg)
self._write_optional_child("returns", stmt.returns, last=True)
def _print_argument(self, arg: p.Function.Argument) -> None:
self._write_line("FunctionArgument")
with self._child_level():
self._write_line(f"name: {arg.name}")
self._write_optional_child("type", arg.type, last=True)
def visit_type_assign(self, stmt: p.TypeAssign) -> None:
self._write_line("TypeAssign")
with self._child_level():
self._write_line(f"name: {stmt.name}")
self._write_line("type", last=True)
with self._child_level(single=True):
stmt.type.accept(self)
def visit_assign_expr(self, expr: p.AssignExpr) -> None:
self._write_line("AssignExpr")
with self._child_level():
self._write_line(f"name: {expr.name}")
self._write_line("value", last=True)
with self._child_level(single=True):
expr.value.accept(self)
def visit_binary_expr(self, expr: p.BinaryExpr) -> None:
self._write_line("BinaryExpr")
with self._child_level():
self._write_line("left")
with self._child_level(single=True):
expr.left.accept(self)
self._write_line(f"operator: {expr.operator.__class__.__name__}")
self._write_line("right", last=True)
with self._child_level(single=True):
expr.right.accept(self)
def visit_unary_expr(self, expr: p.UnaryExpr) -> None:
self._write_line("UnaryExpr")
with self._child_level():
self._write_line(f"operator: {expr.operator.__class__.__name__}")
self._write_line("right", last=True)
with self._child_level(single=True):
expr.right.accept(self)
def visit_call_expr(self, expr: p.CallExpr) -> None:
self._write_line("CallExpr")
with self._child_level():
self._write_line("callee")
with self._child_level(single=True):
expr.callee.accept(self)
self._write_line("arguments", last=True)
with self._child_level():
for i, arg in enumerate(expr.arguments):
self._idx = i
if i == len(expr.arguments) - 1:
self._mark_last() self._mark_last()
arg.accept(self) arg.accept(self)
self._write_optional_child("returns", node.returns, last=True) def visit_get_expr(self, expr: p.GetExpr) -> None:
self._write_line("GetExpr")
def visit_function_argument(self, node: p.FunctionArgument) -> None:
self._write_line("FunctionArgument")
with self._child_level(): with self._child_level():
self._write_line(f"name: {node.name}") self._write_line("object")
self._write_optional_child("type", node.type, last=True) with self._child_level(single=True):
expr.object.accept(self)
self._write_line(f"name: {expr.name}", last=True)
def visit_literal_expr(self, expr: p.LiteralExpr) -> None:
self._write_line("LiteralExpr")
with self._child_level(single=True):
self._write_line(f"value: {expr.value}")
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
self._write_line("VariableExpr")
with self._child_level(single=True):
self._write_line(f"name: {expr.name}")
def visit_logical_expr(self, expr: p.LogicalExpr) -> None:
self._write_line("LogicalExpr")
with self._child_level():
self._write_line("left")
with self._child_level(single=True):
expr.left.accept(self)
self._write_line(f"operator: {expr.operator.__class__.__name__}")
self._write_line("right", last=True)
with self._child_level(single=True):
expr.right.accept(self)
def visit_set_expr(self, expr: p.SetExpr) -> None:
self._write_line("SetExpr")
with self._child_level():
self._write_line("object")
with self._child_level(single=True):
expr.object.accept(self)
self._write_line(f"name: {expr.name}")
self._write_line("value", last=True)
with self._child_level(single=True):
expr.value.accept(self)
+185 -27
View File
@@ -1,17 +1,26 @@
"""
This file was generated by a script. Any manual changes might be overwritten.
Please modify gen/python.py instead and run gen/gen.py
"""
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod
import ast import ast
from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from typing import Generic, Optional, TypeVar from typing import Any, Generic, Optional, TypeVar
from midas.ast.location import Location from midas.ast.location import Location
T = TypeVar("T") T = TypeVar("T")
####################
# Type annotations #
####################
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class Expr(ABC): class MidasType(ABC):
location: Optional[Location] = None location: Optional[Location] = None
@abstractmethod @abstractmethod
@@ -30,24 +39,13 @@ class Expr(ABC):
@abstractmethod @abstractmethod
def visit_frame_type(self, node: FrameType) -> T: ... def visit_frame_type(self, node: FrameType) -> T: ...
@abstractmethod
def visit_function(self, node: Function) -> T: ...
@abstractmethod
def visit_function_argument(self, node: FunctionArgument) -> T: ...
@dataclass(frozen=True)
class MidasType(Expr):
pass
@dataclass(frozen=True) @dataclass(frozen=True)
class BaseType(MidasType): class BaseType(MidasType):
base: str base: str
param: Optional[MidasType] param: Optional[MidasType]
def accept(self, visitor: Expr.Visitor[T]) -> T: def accept(self, visitor: MidasType.Visitor[T]) -> T:
return visitor.visit_base_type(self) return visitor.visit_base_type(self)
@@ -56,7 +54,7 @@ class ConstraintType(MidasType):
type: MidasType type: MidasType
constraint: ast.expr constraint: ast.expr
def accept(self, visitor: Expr.Visitor[T]) -> T: def accept(self, visitor: MidasType.Visitor[T]) -> T:
return visitor.visit_constraint_type(self) return visitor.visit_constraint_type(self)
@@ -65,7 +63,7 @@ class FrameColumn(MidasType):
name: Optional[str] name: Optional[str]
type: Optional[MidasType] type: Optional[MidasType]
def accept(self, visitor: Expr.Visitor[T]) -> T: def accept(self, visitor: MidasType.Visitor[T]) -> T:
return visitor.visit_frame_column(self) return visitor.visit_frame_column(self)
@@ -73,26 +71,186 @@ class FrameColumn(MidasType):
class FrameType(MidasType): class FrameType(MidasType):
columns: list[FrameColumn] columns: list[FrameColumn]
def accept(self, visitor: Expr.Visitor[T]) -> T: def accept(self, visitor: MidasType.Visitor[T]) -> T:
return visitor.visit_frame_type(self) return visitor.visit_frame_type(self)
##############
# Statements #
##############
@dataclass(frozen=True, kw_only=True)
class Stmt(ABC):
location: Optional[Location] = None
@abstractmethod
def accept(self, visitor: Visitor[T]) -> T: ...
class Visitor(ABC, Generic[T]):
@abstractmethod
def visit_expression_stmt(self, stmt: ExpressionStmt) -> T: ...
@abstractmethod
def visit_function(self, stmt: Function) -> T: ...
@abstractmethod
def visit_type_assign(self, stmt: TypeAssign) -> T: ...
@dataclass(frozen=True) @dataclass(frozen=True)
class Function(Expr): class ExpressionStmt(Stmt):
expr: Expr
def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_expression_stmt(self)
@dataclass(frozen=True)
class Function(Stmt):
name: str name: str
posonlyargs: list[FunctionArgument] posonlyargs: list[Argument]
args: list[FunctionArgument] args: list[Argument]
kwonlyargs: list[FunctionArgument] kwonlyargs: list[Argument]
returns: Optional[MidasType] returns: Optional[MidasType]
def accept(self, visitor: Expr.Visitor[T]) -> T: @dataclass(frozen=True, kw_only=True)
class Argument:
location: Optional[Location] = None
name: Optional[str]
type: Optional[MidasType]
def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_function(self) return visitor.visit_function(self)
@dataclass(frozen=True) @dataclass(frozen=True)
class FunctionArgument(Expr): class TypeAssign(Stmt):
name: Optional[str] name: str
type: Optional[MidasType] type: MidasType
def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_type_assign(self)
###############
# Expressions #
###############
@dataclass(frozen=True, kw_only=True)
class Expr(ABC):
location: Optional[Location] = None
@abstractmethod
def accept(self, visitor: Visitor[T]) -> T: ...
class Visitor(ABC, Generic[T]):
@abstractmethod
def visit_assign_expr(self, expr: AssignExpr) -> T: ...
@abstractmethod
def visit_binary_expr(self, expr: BinaryExpr) -> T: ...
@abstractmethod
def visit_unary_expr(self, expr: UnaryExpr) -> T: ...
@abstractmethod
def visit_call_expr(self, expr: CallExpr) -> T: ...
@abstractmethod
def visit_get_expr(self, expr: GetExpr) -> T: ...
@abstractmethod
def visit_literal_expr(self, expr: LiteralExpr) -> T: ...
@abstractmethod
def visit_variable_expr(self, expr: VariableExpr) -> T: ...
@abstractmethod
def visit_logical_expr(self, expr: LogicalExpr) -> T: ...
@abstractmethod
def visit_set_expr(self, expr: SetExpr) -> T: ...
@dataclass(frozen=True)
class AssignExpr(Expr):
name: str
value: Expr
def accept(self, visitor: Expr.Visitor[T]) -> T: def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_function_argument(self) return visitor.visit_assign_expr(self)
@dataclass(frozen=True)
class BinaryExpr(Expr):
left: Expr
operator: ast.operator
right: Expr
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_binary_expr(self)
@dataclass(frozen=True)
class UnaryExpr(Expr):
operator: ast.unaryop
right: Expr
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_unary_expr(self)
@dataclass(frozen=True)
class CallExpr(Expr):
callee: Expr
arguments: list[Expr]
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_call_expr(self)
@dataclass(frozen=True)
class GetExpr(Expr):
object: Expr
name: str
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_get_expr(self)
@dataclass(frozen=True)
class LiteralExpr(Expr):
value: Any
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_literal_expr(self)
@dataclass(frozen=True)
class VariableExpr(Expr):
name: str
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_variable_expr(self)
@dataclass(frozen=True)
class LogicalExpr(Expr):
left: Expr
operator: ast.boolop
right: Expr
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_logical_expr(self)
@dataclass(frozen=True)
class SetExpr(Expr):
object: Expr
name: str
value: Expr
def accept(self, visitor: Expr.Visitor[T]) -> T:
return visitor.visit_set_expr(self)
+69 -26
View File
@@ -1,15 +1,20 @@
import ast import ast
from typing import Any, Optional from typing import Optional
from midas.ast.location import Location from midas.ast.location import Location
from midas.ast.python import ( from midas.ast.python import (
AssignExpr,
BaseType, BaseType,
ConstraintType, ConstraintType,
Expr,
ExpressionStmt,
FrameColumn, FrameColumn,
FrameType, FrameType,
Function, Function,
FunctionArgument,
MidasType, MidasType,
Stmt,
TypeAssign,
) )
@@ -24,33 +29,66 @@ class UnsupportedSyntaxError(Exception):
) )
class PythonParser(ast.NodeVisitor): class PythonParser:
def __init__(self) -> None: def parse_module(self, node: ast.Module) -> list[Stmt]:
super().__init__() statements: list[Stmt] = []
for stmt in node.body:
parsed: None | Stmt | list[Stmt] = self.parse_stmt(stmt)
if isinstance(parsed, Stmt):
statements.append(parsed)
elif parsed is not None:
statements.extend(parsed)
return statements
self.annotations: list[tuple[str, Optional[MidasType]]] = [] def parse_stmt(self, node: ast.stmt) -> None | Stmt | list[Stmt]:
self.functions: list[Function] = []
def visit_AnnAssign(self, node: ast.AnnAssign) -> Any:
match node: match node:
case ast.AnnAssign( case ast.AnnAssign():
target=ast.Name(id=target), annotation=annotation, simple=1 return self.parse_annotation_assign(node)
):
self.annotations.append( case ast.FunctionDef():
(target, self._parse_type(annotation, root=True)) return self.parse_function(node)
)
case _:
print(f"Unsupported assignment: {ast.unparse(node)}")
return None
def parse_annotation_assign(self, node: ast.AnnAssign) -> list[Stmt]:
statements: list[Stmt] = []
loc: Location = Location.from_ast(node)
match node:
case ast.AnnAssign(
target=ast.Name(id=target),
annotation=annotation,
value=value,
simple=1,
):
type = self._parse_type(annotation, root=True)
if type is not None:
statements.append(
TypeAssign(
location=loc,
name=target,
type=type,
)
)
if value is not None:
parsed_value: Expr = self.parse_expr(value)
statements.append(
ExpressionStmt(
location=loc,
expr=AssignExpr(
location=loc,
name=target,
value=parsed_value,
),
)
)
case _: case _:
print(f"Unsupported annotation: {ast.unparse(node)}") print(f"Unsupported annotation: {ast.unparse(node)}")
return statements
def visit_FunctionDef(self, node: ast.FunctionDef) -> Any: def parse_function(self, node: ast.FunctionDef) -> Function:
self.functions.append(self._parse_function(node))
# Call visit on children to process body
# TODO: scope the resulting nodes to the function
self.generic_visit(node)
def _parse_function(self, node: ast.FunctionDef) -> Function:
loc: Location = Location.from_ast(node) loc: Location = Location.from_ast(node)
match node: match node:
case ast.FunctionDef( case ast.FunctionDef(
@@ -63,7 +101,7 @@ class PythonParser(ast.NodeVisitor):
returns=returns, returns=returns,
): ):
def parse_args(args_list: list[ast.arg]) -> list[FunctionArgument]: def parse_args(args_list: list[ast.arg]) -> list[Function.Argument]:
return [self._parse_function_argument(arg) for arg in args_list] return [self._parse_function_argument(arg) for arg in args_list]
return Function( return Function(
@@ -74,14 +112,16 @@ class PythonParser(ast.NodeVisitor):
kwonlyargs=parse_args(kwonlyargs), kwonlyargs=parse_args(kwonlyargs),
returns=self._parse_type(returns) if returns is not None else None, returns=self._parse_type(returns) if returns is not None else None,
) )
case _:
print(f"Unsupported function definition: {ast.unparse(node)}")
def _parse_function_argument(self, arg: ast.arg) -> FunctionArgument: def _parse_function_argument(self, arg: ast.arg) -> Function.Argument:
loc: Location = Location.from_ast(arg) loc: Location = Location.from_ast(arg)
name: str = arg.arg name: str = arg.arg
type: Optional[MidasType] = None type: Optional[MidasType] = None
if arg.annotation is not None: if arg.annotation is not None:
type = self._parse_type(arg.annotation) type = self._parse_type(arg.annotation)
return FunctionArgument( return Function.Argument(
location=loc, location=loc,
name=name, name=name,
type=type, type=type,
@@ -186,3 +226,6 @@ class PythonParser(ast.NodeVisitor):
case _: case _:
raise UnsupportedSyntaxError(column) raise UnsupportedSyntaxError(column)
def parse_expr(self, node: ast.expr) -> Expr:
raise NotImplementedError()