From ede7272c097d75e770cf3e24df88669943f059a4 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 23 Jun 2026 15:56:07 +0200 Subject: [PATCH] feat(parser): add frame type to midas syntax --- gen/midas.py | 10 ++++++++++ midas/ast/midas.py | 17 +++++++++++++++++ midas/ast/printer.py | 36 ++++++++++++++++++++++++++++++++++++ midas/parser/midas.py | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/gen/midas.py b/gen/midas.py index 4c4ca32..fed8626 100644 --- a/gen/midas.py +++ b/gen/midas.py @@ -157,4 +157,14 @@ class FunctionType: required: bool +class FrameType: + columns: list[Column] + + @dataclass(frozen=True, kw_only=True) + class Column: + location: Optional[Location] = None + name: Token + type: Type + + ###< diff --git a/midas/ast/midas.py b/midas/ast/midas.py index a6e2ed9..128b1c0 100644 --- a/midas/ast/midas.py +++ b/midas/ast/midas.py @@ -265,6 +265,9 @@ class Type(ABC): @abstractmethod def visit_function_type(self, type: FunctionType) -> T: ... + @abstractmethod + def visit_frame_type(self, type: FrameType) -> T: ... + @dataclass(frozen=True) class NamedType(Type): @@ -323,3 +326,17 @@ class FunctionType(Type): def accept(self, visitor: Type.Visitor[T]) -> T: return visitor.visit_function_type(self) + + +@dataclass(frozen=True) +class FrameType(Type): + columns: list[Column] + + @dataclass(frozen=True, kw_only=True) + class Column: + location: Optional[Location] = None + name: Token + type: Type + + def accept(self, visitor: Type.Visitor[T]) -> T: + return visitor.visit_frame_type(self) diff --git a/midas/ast/printer.py b/midas/ast/printer.py index 17f0dd0..d713a74 100644 --- a/midas/ast/printer.py +++ b/midas/ast/printer.py @@ -358,6 +358,25 @@ class MidasAstPrinter( arg.type.accept(self) self._write_line(f"required: {arg.required}", last=True) + def visit_frame_type(self, type: m.FrameType) -> None: + self._write_line("FrameType") + with self._child_level(single=True): + self._write_line("columns") + with self._child_level(): + for i, column in enumerate(type.columns): + self._idx = i + if i == len(type.columns) - 1: + self._mark_last() + self._print_frame_column(column) + + def _print_frame_column(self, column: m.FrameType.Column) -> None: + self._write_line("Column") + with self._child_level(): + self._write_line(f'name: "{column.name.lexeme}"') + self._write_line("type") + with self._child_level(single=True): + column.type.accept(self) + class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]): def __init__(self, indent: int = 4): @@ -513,6 +532,23 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str] res += "?" return res + def visit_frame_type(self, type: m.FrameType) -> str: + res: str = self.indented("Frame[") + if len(type.columns) != 0: + res += "\n" + self.level += 1 + columns: list[str] = [] + for column in type.columns: + columns.append(self.indented(self._print_frame_column(column))) + res += ",\n".join(columns) + self.level -= 1 + res += "\n" + res += "]" + return res + + def _print_frame_column(self, column: m.FrameType.Column) -> str: + return f"{column.name.lexeme}: {column.type.accept(self)}" + class PythonAstPrinter( AstPrinter, diff --git a/midas/parser/midas.py b/midas/parser/midas.py index c5fced1..77ad40b 100644 --- a/midas/parser/midas.py +++ b/midas/parser/midas.py @@ -10,6 +10,7 @@ from midas.ast.midas import ( Expr, ExtendStmt, ExtensionType, + FrameType, FunctionType, GenericType, GetExpr, @@ -226,8 +227,10 @@ class MidasParser(Parser): return self.generic_type() def generic_type(self) -> Type: - type: Type = self.named_type() + type: NamedType = self.named_type() if self.check(TokenType.LEFT_BRACKET): + if type.name.lexeme == "Frame": + return self.frame_type() args: list[Type] = self.type_args() return GenericType( location=Location.span(type.location, self.previous().get_location()), @@ -246,7 +249,7 @@ class MidasParser(Parser): self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after generic arguments") return args - def named_type(self) -> Type: + def named_type(self) -> NamedType: name: Token = self.consume_identifier("Expected type name") return NamedType( location=name.get_location(), @@ -281,6 +284,32 @@ class MidasParser(Parser): members=members, ) + def frame_type(self) -> FrameType: + keyword: Token = self.previous() + self.consume(TokenType.LEFT_BRACKET, "Expected '[' to start frame schema") + + columns: list[FrameType.Column] = [] + while not self.check(TokenType.RIGHT_BRACKET) and not self.is_at_end(): + name: Token = self.advance() + self.consume(TokenType.COLON, "Expected ':' between column name and type") + type: Type = self.type_expr() + columns.append( + FrameType.Column( + location=name.location_to(self.previous()), + name=name, + type=type, + ) + ) + if not self.match(TokenType.COMMA): + break + + self.consume(TokenType.RIGHT_BRACKET, "Unclosed frame schema") + + return FrameType( + location=keyword.location_to(self.previous()), + columns=columns, + ) + def constraint(self) -> Expr: """Parse a constraint