diff --git a/gen/midas.py b/gen/midas.py index 287fcc3..faa3c52 100644 --- a/gen/midas.py +++ b/gen/midas.py @@ -152,4 +152,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 1ece261..38556b2 100644 --- a/midas/ast/midas.py +++ b/midas/ast/midas.py @@ -253,6 +253,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): @@ -311,3 +314,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 680fd79..c2c0704 100644 --- a/midas/ast/printer.py +++ b/midas/ast/printer.py @@ -350,6 +350,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): @@ -502,6 +521,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 fdb58c2..8696b61 100644 --- a/midas/parser/midas.py +++ b/midas/parser/midas.py @@ -9,6 +9,7 @@ from midas.ast.midas import ( Expr, ExtendStmt, ExtensionType, + FrameType, FunctionType, GenericType, GetExpr, @@ -204,8 +205,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()), @@ -224,7 +227,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(), @@ -259,6 +262,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