From 505779310aedab8e37ff4477860a22eeaa076007 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 11:40:42 +0200 Subject: [PATCH 01/22] feat: add new midas syntax example --- .../05_custom_types_v3.midas | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 examples/00_syntax_prototype/05_custom_types_v3.midas diff --git a/examples/00_syntax_prototype/05_custom_types_v3.midas b/examples/00_syntax_prototype/05_custom_types_v3.midas new file mode 100644 index 0000000..a339318 --- /dev/null +++ b/examples/00_syntax_prototype/05_custom_types_v3.midas @@ -0,0 +1,33 @@ +type Foo1 = float +type Foo2 = float where (_ > 3) +type Foo3 = int | float +type Foo4 = int where (_ > 3) | float where (_ > 3) +type Foo5 = (int | float) where (_ > 3) +type Foo6 = { + foo: float + bar: float where (_ > 3) +} + +type Foo7[T] = T where (_ > 3) +type Foo8[A, B<:int] = { + a: A + b: B +} + +type Complex = { + a: int + b: int +} +type Complex2 = Complex where (_.a > 3 & _.b < 5) + +predicate Positive(n: int) = n >= 0 + +extend Foo1 { + op __add__(Foo1) -> Foo1 +} + +extend Foo7[T] { + op __add__(Foo7[T]) -> Foo7[T] +} + +type Optional[T] = None | T -- 2.49.1 From ccb17c729099b595eb87c311caba80f3ceb8feca Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 11:41:53 +0200 Subject: [PATCH 02/22] feat(parser)!: add new Midas AST nodes --- gen/midas.py | 58 +++++++++------ midas/ast/midas.py | 132 +++++++++++++++++++-------------- midas/ast/printer.py | 170 +++++++++++++++++++++++++++---------------- 3 files changed, 221 insertions(+), 139 deletions(-) diff --git a/gen/midas.py b/gen/midas.py index 7187554..dc0efd0 100644 --- a/gen/midas.py +++ b/gen/midas.py @@ -13,40 +13,38 @@ from midas.lexer.token import Token ###> Stmt | Statements -class SimpleTypeStmt: +class TypeStmt: name: Token - template: Optional[TemplateExpr] - base: TypeExpr - constraint: Optional[Expr] + params: list[Param] + type: Type - -class ComplexTypeStmt: - name: Token - template: Optional[TemplateExpr] - properties: list[PropertyStmt] + @dataclass(frozen=True, kw_only=True) + class Param: + location: Location + name: Token + bound: Optional[Type] class PropertyStmt: name: Token - type: TypeExpr - constraint: Optional[Expr] + type: Type class ExtendStmt: - type: TypeExpr + type: Type operations: list[OpStmt] class OpStmt: name: Token - operand: TypeExpr - result: TypeExpr + operand: Type + result: Type class PredicateStmt: name: Token subject: Token - type: TypeExpr + type: Type condition: Expr @@ -54,9 +52,6 @@ class PredicateStmt: ###> Expr | Expressions -class SimpleTypeExpr: - name: Token - optional: bool class LogicalExpr: @@ -97,14 +92,31 @@ class WildcardExpr: token: Token -class TemplateExpr: - type: TypeExpr +###< + +###> Type | Types -class TypeExpr: +class NamedType: name: Token - template: Optional[TemplateExpr] - optional: bool + + +class GenericType: + type: Type + params: list[Type] + + +class ConstraintType: + type: Type + constraint: Expr + + +class UnionType: + types: list[Type] + + +class ComplexType: + properties: list[PropertyStmt] ###< diff --git a/midas/ast/midas.py b/midas/ast/midas.py index c307e85..a18b460 100644 --- a/midas/ast/midas.py +++ b/midas/ast/midas.py @@ -28,10 +28,7 @@ class Stmt(ABC): class Visitor(ABC, Generic[T]): @abstractmethod - def visit_simple_type_stmt(self, stmt: SimpleTypeStmt) -> T: ... - - @abstractmethod - def visit_complex_type_stmt(self, stmt: ComplexTypeStmt) -> T: ... + def visit_type_stmt(self, stmt: TypeStmt) -> T: ... @abstractmethod def visit_property_stmt(self, stmt: PropertyStmt) -> T: ... @@ -47,31 +44,25 @@ class Stmt(ABC): @dataclass(frozen=True) -class SimpleTypeStmt(Stmt): +class TypeStmt(Stmt): name: Token - template: Optional[TemplateExpr] - base: TypeExpr - constraint: Optional[Expr] + params: list[Param] + type: Type + + @dataclass(frozen=True, kw_only=True) + class Param: + location: Location + name: Token + bound: Optional[Type] def accept(self, visitor: Stmt.Visitor[T]) -> T: - return visitor.visit_simple_type_stmt(self) - - -@dataclass(frozen=True) -class ComplexTypeStmt(Stmt): - name: Token - template: Optional[TemplateExpr] - properties: list[PropertyStmt] - - def accept(self, visitor: Stmt.Visitor[T]) -> T: - return visitor.visit_complex_type_stmt(self) + return visitor.visit_type_stmt(self) @dataclass(frozen=True) class PropertyStmt(Stmt): name: Token - type: TypeExpr - constraint: Optional[Expr] + type: Type def accept(self, visitor: Stmt.Visitor[T]) -> T: return visitor.visit_property_stmt(self) @@ -79,7 +70,7 @@ class PropertyStmt(Stmt): @dataclass(frozen=True) class ExtendStmt(Stmt): - type: TypeExpr + type: Type operations: list[OpStmt] def accept(self, visitor: Stmt.Visitor[T]) -> T: @@ -89,8 +80,8 @@ class ExtendStmt(Stmt): @dataclass(frozen=True) class OpStmt(Stmt): name: Token - operand: TypeExpr - result: TypeExpr + operand: Type + result: Type def accept(self, visitor: Stmt.Visitor[T]) -> T: return visitor.visit_op_stmt(self) @@ -100,7 +91,7 @@ class OpStmt(Stmt): class PredicateStmt(Stmt): name: Token subject: Token - type: TypeExpr + type: Type condition: Expr def accept(self, visitor: Stmt.Visitor[T]) -> T: @@ -120,9 +111,6 @@ class Expr(ABC): def accept(self, visitor: Visitor[T]) -> T: ... class Visitor(ABC, Generic[T]): - @abstractmethod - def visit_simple_type_expr(self, expr: SimpleTypeExpr) -> T: ... - @abstractmethod def visit_logical_expr(self, expr: LogicalExpr) -> T: ... @@ -147,21 +135,6 @@ class Expr(ABC): @abstractmethod def visit_wildcard_expr(self, expr: WildcardExpr) -> T: ... - @abstractmethod - def visit_template_expr(self, expr: TemplateExpr) -> T: ... - - @abstractmethod - def visit_type_expr(self, expr: TypeExpr) -> T: ... - - -@dataclass(frozen=True) -class SimpleTypeExpr(Expr): - name: Token - optional: bool - - def accept(self, visitor: Expr.Visitor[T]) -> T: - return visitor.visit_simple_type_expr(self) - @dataclass(frozen=True) class LogicalExpr(Expr): @@ -233,19 +206,72 @@ class WildcardExpr(Expr): return visitor.visit_wildcard_expr(self) -@dataclass(frozen=True) -class TemplateExpr(Expr): - type: TypeExpr +######### +# Types # +######### - def accept(self, visitor: Expr.Visitor[T]) -> T: - return visitor.visit_template_expr(self) + +@dataclass(frozen=True, kw_only=True) +class Type(ABC): + location: Location + + @abstractmethod + def accept(self, visitor: Visitor[T]) -> T: ... + + class Visitor(ABC, Generic[T]): + @abstractmethod + def visit_named_type(self, type: NamedType) -> T: ... + + @abstractmethod + def visit_generic_type(self, type: GenericType) -> T: ... + + @abstractmethod + def visit_constraint_type(self, type: ConstraintType) -> T: ... + + @abstractmethod + def visit_union_type(self, type: UnionType) -> T: ... + + @abstractmethod + def visit_complex_type(self, type: ComplexType) -> T: ... @dataclass(frozen=True) -class TypeExpr(Expr): +class NamedType(Type): name: Token - template: Optional[TemplateExpr] - optional: bool - def accept(self, visitor: Expr.Visitor[T]) -> T: - return visitor.visit_type_expr(self) + def accept(self, visitor: Type.Visitor[T]) -> T: + return visitor.visit_named_type(self) + + +@dataclass(frozen=True) +class GenericType(Type): + type: Type + params: list[Type] + + def accept(self, visitor: Type.Visitor[T]) -> T: + return visitor.visit_generic_type(self) + + +@dataclass(frozen=True) +class ConstraintType(Type): + type: Type + constraint: Expr + + def accept(self, visitor: Type.Visitor[T]) -> T: + return visitor.visit_constraint_type(self) + + +@dataclass(frozen=True) +class UnionType(Type): + types: list[Type] + + def accept(self, visitor: Type.Visitor[T]) -> T: + return visitor.visit_union_type(self) + + +@dataclass(frozen=True) +class ComplexType(Type): + properties: list[PropertyStmt] + + def accept(self, visitor: Type.Visitor[T]) -> T: + return visitor.visit_complex_type(self) diff --git a/midas/ast/printer.py b/midas/ast/printer.py index 45e4a64..ed9e069 100644 --- a/midas/ast/printer.py +++ b/midas/ast/printer.py @@ -85,40 +85,39 @@ class AstPrinter(Generic[T]): child.accept(self) -class MidasAstPrinter(AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None]): +class MidasAstPrinter( + AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None], m.Type.Visitor[None] +): # Statements - def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt): - self._write_line("SimpleTypeStmt") + def visit_type_stmt(self, stmt: m.TypeStmt) -> None: + self._write_line("TypeStmt") with self._child_level(): self._write_line(f'name: "{stmt.name.lexeme}"') - self._write_optional_child("template", stmt.template) - self._write_line("base") - with self._child_level(single=True): - stmt.base.accept(self) - self._write_optional_child("constraint", stmt.constraint, last=True) - - def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt): - self._write_line("ComplexTypeStmt") - with self._child_level(): - self._write_line(f'name: "{stmt.name.lexeme}"') - self._write_optional_child("template", stmt.template) - self._write_line("properties", last=True) + self._write_line("params") with self._child_level(): - for i, prop in enumerate(stmt.properties): + for i, param in enumerate(stmt.params): self._idx = i - if i == len(stmt.properties) - 1: + if i == len(stmt.params) - 1: self._mark_last() - prop.accept(self) + self._print_type_stmt_param(param) + self._write_line("type", last=True) + with self._child_level(single=True): + stmt.type.accept(self) + + def _print_type_stmt_param(self, param: m.TypeStmt.Param) -> None: + self._write_line("Param") + with self._child_level(): + self._write_line(f'name: "{param.name.lexeme}"') + self._write_optional_child("bound", param.bound, last=True) def visit_property_stmt(self, stmt: m.PropertyStmt): self._write_line("PropertyStmt") with self._child_level(): self._write_line(f'name: "{stmt.name.lexeme}"') - self._write_line("type") + self._write_line("type", last=True) with self._child_level(single=True): stmt.type.accept(self) - self._write_optional_child("constraint", stmt.constraint, last=True) def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None: self._write_line("ExtendStmt") @@ -161,12 +160,6 @@ class MidasAstPrinter(AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None]): # Expressions - def visit_simple_type_expr(self, expr: m.SimpleTypeExpr): - self._write_line("SimpleTypeExpr") - with self._child_level(): - self._write_line(f'name: "{expr.name.lexeme}"') - self._write_line(f"optional: {expr.optional}", last=True) - def visit_logical_expr(self, expr: m.LogicalExpr): self._write_line("LogicalExpr") with self._child_level(): @@ -230,22 +223,59 @@ class MidasAstPrinter(AstPrinter, m.Expr.Visitor[None], m.Stmt.Visitor[None]): def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None: self._write_line("WildcardExpr") - def visit_template_expr(self, expr: m.TemplateExpr) -> None: - self._write_line("TemplateExpr") - with self._child_level(single=True): + def visit_named_type(self, type: m.NamedType) -> None: + self._write_line("NamedType") + with self._child_level(): + self._write_line(f'name: "{type.name.lexeme}"', last=True) + + def visit_generic_type(self, type: m.GenericType) -> None: + self._write_line("GenericType") + with self._child_level(): + self._write_line("type") + with self._child_level(): + type.type.accept(self) + self._write_line("params", last=True) + with self._child_level(): + for i, param in enumerate(type.params): + self._idx = i + if i == len(type.params) - 1: + self._mark_last() + param.accept(self) + + def visit_constraint_type(self, type: m.ConstraintType) -> None: + self._write_line("ConstraintType") + with self._child_level(): self._write_line("type") with self._child_level(single=True): - expr.type.accept(self) + type.type.accept(self) + self._write_line("constraint", last=True) + with self._child_level(single=True): + type.constraint.accept(self) - def visit_type_expr(self, expr: m.TypeExpr): - self._write_line("TypeExpr") + def visit_union_type(self, type: m.UnionType) -> None: + self._write_line("UnionType") with self._child_level(): - self._write_line(f'name: "{expr.name.lexeme}"') - self._write_optional_child("template", expr.template) - self._write_line(f"optional: {expr.optional}", last=True) + self._write_line("types", last=True) + with self._child_level(): + for i, type_ in enumerate(type.types): + self._idx = i + if i == len(type.types) - 1: + self._mark_last() + type_.accept(self) + + def visit_complex_type(self, type: m.ComplexType) -> None: + self._write_line("ComplexType") + with self._child_level(): + self._write_line("properties", last=True) + with self._child_level(): + for i, prop in enumerate(type.properties): + self._idx = i + if i == len(type.properties) - 1: + self._mark_last() + prop.accept(self) -class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]): +class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str]): def __init__(self, indent: int = 4): self.indent: int = indent self.level: int = 0 @@ -257,29 +287,24 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]): self.level = 0 return expr.accept(self) - def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt): - template: str = stmt.template.accept(self) if stmt.template is not None else "" - res: str = f"type {stmt.name.lexeme}{template}({stmt.base.accept(self)})" - if stmt.constraint is not None: - res += " where " + stmt.constraint.accept(self) + def visit_type_stmt(self, stmt: m.TypeStmt) -> str: + template: str = "" + if len(stmt.params) != 0: + params: list[str] = [ + self._print_type_template_param(param) for param in stmt.params + ] + template = f"[{', '.join(params)}]" + res: str = f"type {stmt.name.lexeme}{template} = {stmt.type.accept(self)}" return self.indented(res) - def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt): - template: str = stmt.template.accept(self) if stmt.template is not None else "" - res: str = self.indented(f"type {stmt.name.lexeme}{template}") - res += " {\n" - self.level += 1 - for prop in stmt.properties: - res += prop.accept(self) - res += "\n" - self.level -= 1 - res += self.indented("}") + def _print_type_template_param(self, param: m.TypeStmt.Param) -> str: + res: str = param.name.lexeme + if param.bound is not None: + res += "<:" + param.bound.accept(self) return res def visit_property_stmt(self, stmt: m.PropertyStmt): res: str = f"{stmt.name.lexeme}: {stmt.type.accept(self)}" - if stmt.constraint is not None: - res += " where " + stmt.constraint.accept(self) return self.indented(res) def visit_extend_stmt(self, stmt: m.ExtendStmt): @@ -304,9 +329,6 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]): condition: str = stmt.condition.accept(self) return self.indented(f"predicate {name}({subject}: {type}) = {condition}") - def visit_simple_type_expr(self, expr: m.SimpleTypeExpr): - return f"{expr.name.lexeme}{'?' if expr.optional else ''}" - def visit_logical_expr(self, expr: m.LogicalExpr): left: str = expr.left.accept(self) operator: str = expr.operator.lexeme @@ -342,12 +364,34 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str]): def visit_wildcard_expr(self, expr: m.WildcardExpr): return "_" - def visit_template_expr(self, expr: m.TemplateExpr): - return f"[{expr.type.accept(self)}]" + def visit_named_type(self, type: m.NamedType) -> str: + return type.name.lexeme - def visit_type_expr(self, expr: m.TypeExpr): - template: str = expr.template.accept(self) if expr.template is not None else "" - return f"{expr.name.lexeme}{template}{'?' if expr.optional else ''}" + def visit_generic_type(self, type: m.GenericType) -> str: + res: str = type.type.accept(self) + if len(type.params) != 0: + params: list[str] = [param.accept(self) for param in type.params] + res += f"[{', '.join(params)}]" + return res + + def visit_constraint_type(self, type: m.ConstraintType) -> str: + res: str = type.type.accept(self) + res += " where " + type.constraint.accept(self) + return res + + def visit_union_type(self, type: m.UnionType) -> str: + types: list[str] = [type_.accept(self) for type_ in type.types] + return " | ".join(types) + + def visit_complex_type(self, type: m.ComplexType) -> str: + res: str = "{\n" + self.level += 1 + for prop in type.properties: + res += prop.accept(self) + res += "\n" + self.level -= 1 + res += self.indented("}") + return res class PythonAstPrinter( @@ -600,11 +644,11 @@ class PythonAstPrinter( self._write_line("test") with self._child_level(single=True): expr.test.accept(self) - + self._write_line("if_true") with self._child_level(single=True): expr.if_true.accept(self) - + self._write_line("if_false", last=True) with self._child_level(single=True): expr.if_false.accept(self) -- 2.49.1 From b9f378de6f0c8c137472e45eeffa064f9acb1902 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 11:42:35 +0200 Subject: [PATCH 03/22] feat(parser)!: update Midas parser with new nodes --- midas/lexer/midas.py | 6 +- midas/lexer/token.py | 3 +- midas/parser/midas.py | 214 +++++++++++++++++++++++++----------------- 3 files changed, 133 insertions(+), 90 deletions(-) diff --git a/midas/lexer/midas.py b/midas/lexer/midas.py index acc97d6..eec44d5 100644 --- a/midas/lexer/midas.py +++ b/midas/lexer/midas.py @@ -18,6 +18,8 @@ class MidasLexer(Lexer): self.add_token(TokenType.LEFT_BRACE) case "}": self.add_token(TokenType.RIGHT_BRACE) + case "|": + self.add_token(TokenType.PIPE) case "<": self.add_token( TokenType.LESS_EQUAL if self.match("=") else TokenType.LESS @@ -40,8 +42,8 @@ class MidasLexer(Lexer): self.add_token(TokenType.AND) case "?": self.add_token(TokenType.QMARK) - # case ",": - # self.add_token(TokenType.COMMA) + case ",": + self.add_token(TokenType.COMMA) case "_" if not self.is_identifier_char(self.peek_next(), start=False): self.add_token(TokenType.UNDERSCORE) case "-" if self.match(">"): diff --git a/midas/lexer/token.py b/midas/lexer/token.py index a518a8b..9b30940 100644 --- a/midas/lexer/token.py +++ b/midas/lexer/token.py @@ -17,12 +17,13 @@ class TokenType(Enum): LEFT_BRACE = auto() RIGHT_BRACE = auto() COLON = auto() - # COMMA = auto() + COMMA = auto() UNDERSCORE = auto() ARROW = auto() AND = auto() QMARK = auto() DOT = auto() + PIPE = auto() # Operators # PLUS = auto() diff --git a/midas/parser/midas.py b/midas/parser/midas.py index 5a9d649..9af46da 100644 --- a/midas/parser/midas.py +++ b/midas/parser/midas.py @@ -3,22 +3,24 @@ from typing import Optional from midas.ast.location import Location from midas.ast.midas import ( BinaryExpr, - ComplexTypeStmt, + ComplexType, + ConstraintType, Expr, ExtendStmt, + GenericType, GetExpr, GroupingExpr, LiteralExpr, LogicalExpr, + NamedType, OpStmt, PredicateStmt, PropertyStmt, - SimpleTypeExpr, - SimpleTypeStmt, Stmt, - TemplateExpr, - TypeExpr, + Type, + TypeStmt, UnaryExpr, + UnionType, VariableExpr, WildcardExpr, ) @@ -81,7 +83,7 @@ class MidasParser(Parser): self.synchronize() return None - def type_declaration(self) -> SimpleTypeStmt | ComplexTypeStmt: + def type_declaration(self) -> TypeStmt: """Parse a type declaration A type declaration can either be a simple type alias or a new complex type. @@ -107,33 +109,22 @@ class MidasParser(Parser): """ keyword: Token = self.previous() name: Token = self.consume(TokenType.IDENTIFIER, "Expected type name") - template: Optional[TemplateExpr] = None + params: list[TypeStmt.Param] = [] if self.check(TokenType.LEFT_BRACKET): - template = self.template_expr() + params = self.type_stmt_params() - if self.match(TokenType.LEFT_PAREN): - base: TypeExpr = self.type_expr() - self.consume(TokenType.RIGHT_PAREN, "Unclosed base type parenthesis") - constraint: Optional[Expr] = None - if self.match(TokenType.WHERE): - constraint = self.constraint() - return SimpleTypeStmt( - location=keyword.location_to(self.previous()), - name=name, - template=template, - base=base, - constraint=constraint, - ) - else: - properties: list[PropertyStmt] = self.type_properties() - return ComplexTypeStmt( - location=keyword.location_to(self.previous()), - name=name, - template=template, - properties=properties, - ) + self.consume(TokenType.EQUAL, "Expected '=' before type definition") - def template_expr(self) -> TemplateExpr: + type: Type = self.type_expr() + + return TypeStmt( + location=keyword.location_to(self.previous()), + name=name, + params=params, + type=type, + ) + + def type_stmt_params(self) -> list[TypeStmt.Param]: """Parse a generic template expression A template is written `[TypeExpr]` @@ -141,16 +132,27 @@ class MidasParser(Parser): Returns: TemplateExpr: the parsed template expression """ - left: Token = self.consume( - TokenType.LEFT_BRACKET, "Missing '[' before template expression" - ) - type: TypeExpr = self.type_expr() - right: Token = self.consume( - TokenType.RIGHT_BRACKET, "Missing ']' after template expression" - ) - return TemplateExpr(location=left.location_to(right), type=type) + self.consume(TokenType.LEFT_BRACKET, "Missing '[' before template expression") + params: list[TypeStmt.Param] = [] + while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACKET): + name: Token = self.consume(TokenType.IDENTIFIER, "Expected type variable") + bound: Optional[Type] = None + if self.match(TokenType.LESS): + self.consume(TokenType.COLON, "Expected ':' after '<'") + bound = self.type_expr() + params.append( + TypeStmt.Param( + location=name.location_to(self.previous()), + name=name, + bound=bound, + ) + ) + if not self.match(TokenType.COMMA): + break + self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after template expression") + return params - def type_expr(self) -> TypeExpr: + def type_expr(self) -> Type: """Parse a type expression A type is an identifier, optionally followed by a template expression. @@ -159,30 +161,93 @@ class MidasParser(Parser): Returns: TypeExpr: the parsed type expression """ - name: Token = self.consume(TokenType.IDENTIFIER, "Expected type name") - template: Optional[TemplateExpr] = None - if self.check(TokenType.LEFT_BRACKET): - template = self.template_expr() - optional: bool = self.match(TokenType.QMARK) - return TypeExpr( - location=name.location_to(self.previous()), - name=name, - template=template, - optional=optional, + return self.union_type() + + def union_type(self) -> Type: + types: list[Type] = [self.constraint_type()] + while self.match(TokenType.PIPE): + types.append(self.constraint_type()) + if len(types) == 1: + return types[0] + return UnionType( + location=Location.span(types[0].location, types[-1].location), + types=types, ) - def simple_type_expr(self) -> SimpleTypeExpr: - """Parse a simple type expression + def constraint_type(self) -> Type: + type: Type = self.base_type() + if self.match(TokenType.WHERE): + constraint: Expr = self.constraint() + return ConstraintType( + location=Location.span(type.location, constraint.location), + type=type, + constraint=constraint, + ) + return type - A simple type is just an identifier optionally followed by a '?' + def base_type(self) -> Type: + if self.match(TokenType.LEFT_PAREN): + type: Type = self.type_expr() + self.consume(TokenType.RIGHT_PAREN, "Unclosed parenthesis") + return type + + if self.check(TokenType.LEFT_BRACE): + return self.complex_type() + + return self.generic_type() + + def generic_type(self) -> Type: + type: Type = self.named_type() + if self.check(TokenType.LEFT_BRACKET): + params: list[Type] = self.type_params() + return GenericType( + location=Location.span(type.location, self.previous().get_location()), + type=type, + params=params, + ) + return type + + def type_params(self) -> list[Type]: + params: list[Type] = [] + self.consume(TokenType.LEFT_BRACKET, "Missing '[' before generic parameters") + while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACKET): + params.append(self.type_expr()) + if not self.match(TokenType.COMMA): + break + self.consume(TokenType.RIGHT_BRACKET, "Missing ']' after generic parameters") + return params + + def named_type(self) -> Type: + name: Token = self.consume(TokenType.IDENTIFIER, "Expected type name") + return NamedType( + location=name.get_location(), + name=name, + ) + + def complex_type(self) -> Type: + """Parse a type definition body + + A type definition body is a set of whitespace-separated + property statements enclosed in curly braces Returns: - SimpleTypeExpr: the parsed simple type expression + list[PropertyStmt]: the parsed type properties """ - name: Token = self.consume(TokenType.IDENTIFIER, "Expected type name") - optional: bool = self.match(TokenType.QMARK) - return SimpleTypeExpr( - location=name.location_to(self.previous()), name=name, optional=optional + left: Token = self.consume( + TokenType.LEFT_BRACE, "Expected '{' to start type body" + ) + properties: list[PropertyStmt] = [] + names: set[str] = set() + while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end(): + prop: PropertyStmt = self.property_stmt() + if prop.name.lexeme in names: + raise self.error(prop.name, "Duplicate property") + names.add(prop.name.lexeme) + properties.append(prop) + right: Token = self.consume(TokenType.RIGHT_BRACE, "Unclosed type body") + return ComplexType( + location=left.location_to(right), + properties=properties, ) def constraint(self) -> Expr: @@ -308,27 +373,6 @@ class MidasParser(Parser): raise self.error(self.peek(), "Expected expression") - def type_properties(self) -> list[PropertyStmt]: - """Parse a type definition body - - A type definition body is a set of whitespace-separated - property statements enclosed in curly braces - - Returns: - list[PropertyStmt]: the parsed type properties - """ - self.consume(TokenType.LEFT_BRACE, "Expected '{' to start type body") - properties: list[PropertyStmt] = [] - names: set[str] = set() - while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end(): - prop: PropertyStmt = self.property_stmt() - if prop.name.lexeme in names: - raise self.error(prop.name, "Duplicate property") - names.add(prop.name.lexeme) - properties.append(prop) - self.consume(TokenType.RIGHT_BRACE, "Unclosed type body") - return properties - def property_stmt(self) -> PropertyStmt: """Parse a property statement @@ -339,15 +383,11 @@ class MidasParser(Parser): """ name: Token = self.consume(TokenType.IDENTIFIER, "Expected property name") self.consume(TokenType.COLON, "Expected ':' after property name") - type: TypeExpr = self.type_expr() - constraint: Optional[Expr] = None - if self.match(TokenType.WHERE): - constraint = self.constraint() + type: Type = self.type_expr() return PropertyStmt( location=name.location_to(self.previous()), name=name, type=type, - constraint=constraint, ) def extend_declaration(self) -> ExtendStmt: @@ -359,7 +399,7 @@ class MidasParser(Parser): ExtendStmt: the parsed extension statement """ keyword: Token = self.previous() - type: TypeExpr = self.type_expr() + type: Type = self.type_expr() self.consume(TokenType.LEFT_BRACE, "Expected '{' to start extend body") operations: list[OpStmt] = [] while not self.is_at_end() and not self.check(TokenType.RIGHT_BRACE): @@ -380,11 +420,11 @@ class MidasParser(Parser): name: Token = self.consume(TokenType.IDENTIFIER, "Expected operation name") self.consume(TokenType.LEFT_PAREN, "Expected '(' before operand type") - operand: TypeExpr = self.type_expr() + operand: Type = self.type_expr() self.consume(TokenType.RIGHT_PAREN, "Expected ')' after operand type") self.consume(TokenType.ARROW, "Expected '->' before result type") - result: TypeExpr = self.type_expr() + result: Type = self.type_expr() return OpStmt( location=keyword.location_to(self.previous()), @@ -406,7 +446,7 @@ class MidasParser(Parser): self.consume(TokenType.LEFT_PAREN, "Expected '(' before predicate subject") subject: Token = self.consume(TokenType.IDENTIFIER, "Expected subject name") self.consume(TokenType.COLON, "Expected ':' after subject name") - type: TypeExpr = self.type_expr() + type: Type = self.type_expr() self.consume(TokenType.RIGHT_PAREN, "Expected ')' after predicate subject") self.consume(TokenType.EQUAL, "Expected '=' after predicate subject") condition: Expr = self.constraint() -- 2.49.1 From 7f3d74ee494a1e7eae58294cc64db2285b5752cd Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 11:44:31 +0200 Subject: [PATCH 04/22] feat(checker)!: resolve new types --- midas/checker/types.py | 18 ++++++++-- midas/resolver/midas.py | 73 ++++++++++++++++++++++++----------------- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/midas/checker/types.py b/midas/checker/types.py index 1e2f149..d9d91b7 100644 --- a/midas/checker/types.py +++ b/midas/checker/types.py @@ -9,9 +9,9 @@ class BaseType: @dataclass(frozen=True, kw_only=True) -class SimpleType: +class AliasType: name: str - base: BaseType | SimpleType + type: Type @dataclass(frozen=True, kw_only=True) @@ -39,4 +39,16 @@ class Function: required: bool -Type = BaseType | SimpleType | UnknownType | UnitType | Function +@dataclass(frozen=True, kw_only=True) +class ComplexType: + properties: dict[str, Type] + + +@dataclass(frozen=True, kw_only=True) +class UnionType: + alternatives: list[Type] + + +Type = ( + BaseType | AliasType | UnknownType | UnitType | Function | ComplexType | UnionType +) diff --git a/midas/resolver/midas.py b/midas/resolver/midas.py index d57ca70..ff97dbc 100644 --- a/midas/resolver/midas.py +++ b/midas/resolver/midas.py @@ -1,11 +1,15 @@ from typing import Optional import midas.ast.midas as m -from midas.checker.types import BaseType, SimpleType, Type +from midas.checker.types import ( + Type, + UnionType, + UnknownType, +) from midas.resolver.builtin import define_builtins -class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]): +class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[Type]): """A resolver which evaluates Midas type definitions and build a registry""" def __init__(self) -> None: @@ -94,20 +98,12 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]): for stmt in stmts: stmt.accept(self) - def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt) -> None: - # TODO generics, optional, constraint - base: Type = self.get_type(stmt.base.name.lexeme) - match base: - case BaseType() | SimpleType(): - type = SimpleType( - name=stmt.name.lexeme, - base=base, - ) - self.define_type(type.name, type) - case _: - raise TypeError(f"Invalid base {base} for simple type") - - def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt) -> None: ... + def visit_type_stmt(self, stmt: m.TypeStmt) -> None: + type: Type = stmt.type.accept(self) + for param in stmt.params: + if param.bound is not None: + param.bound.accept(self) + self.define_type(stmt.name.lexeme, type) def visit_property_stmt(self, stmt: m.PropertyStmt) -> None: ... @@ -127,27 +123,44 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[Type]): def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None: ... - def visit_simple_type_expr(self, expr: m.SimpleTypeExpr) -> Type: - return self.get_type(expr.name.lexeme) + def visit_logical_expr(self, expr: m.LogicalExpr) -> None: ... - def visit_logical_expr(self, expr: m.LogicalExpr) -> Type: ... + def visit_binary_expr(self, expr: m.BinaryExpr) -> None: ... - def visit_binary_expr(self, expr: m.BinaryExpr) -> Type: ... + def visit_unary_expr(self, expr: m.UnaryExpr) -> None: ... - def visit_unary_expr(self, expr: m.UnaryExpr) -> Type: ... + def visit_get_expr(self, expr: m.GetExpr) -> None: ... - def visit_get_expr(self, expr: m.GetExpr) -> Type: ... + def visit_variable_expr(self, expr: m.VariableExpr) -> None: ... - def visit_variable_expr(self, expr: m.VariableExpr) -> Type: ... - - def visit_grouping_expr(self, expr: m.GroupingExpr) -> Type: + def visit_grouping_expr(self, expr: m.GroupingExpr) -> None: return expr.expr.accept(self) - def visit_literal_expr(self, expr: m.LiteralExpr) -> Type: ... + def visit_literal_expr(self, expr: m.LiteralExpr) -> None: ... - def visit_wildcard_expr(self, expr: m.WildcardExpr) -> Type: ... + def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None: ... - def visit_template_expr(self, expr: m.TemplateExpr) -> Type: ... + def visit_named_type(self, type: m.NamedType) -> Type: + return self.get_type(type.name.lexeme) - def visit_type_expr(self, expr: m.TypeExpr) -> Type: - return self.get_type(expr.name.lexeme) + def visit_generic_type(self, type: m.GenericType) -> Type: + type_: Type = type.type.accept(self) + params: list[Type] = [param.accept(self) for param in type.params] + # TODO + return UnknownType() + + def visit_constraint_type(self, type: m.ConstraintType) -> Type: + type_: Type = type.type.accept(self) + type.constraint.accept(self) + # TODO + return UnknownType() + + def visit_union_type(self, type: m.UnionType) -> Type: + types: list[Type] = [type_.accept(self) for type_ in type.types] + return UnionType(alternatives=types) + + def visit_complex_type(self, type: m.ComplexType) -> Type: + for prop in type.properties: + prop.accept(self) + # TODO + return UnknownType() -- 2.49.1 From 35ceda99aa4b4374e7c7b8a784bf766aaf8ddec0 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 11:45:49 +0200 Subject: [PATCH 05/22] chore: tidy --- midas/checker/checker.py | 11 +++++++---- midas/resolver/builtin.py | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index c873863..c210b7c 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -9,7 +9,7 @@ from midas.ast.location import Location from midas.checker.diagnostic import Diagnostic, DiagnosticType from midas.checker.environment import Environment from midas.checker.operators import COMPARATOR_METHODS, OPERATOR_METHODS -from midas.checker.types import BaseType, Function, SimpleType, Type, UnitType, UnknownType +from midas.checker.types import Function, Type, UnitType, UnknownType from midas.lexer.midas import MidasLexer from midas.lexer.token import Token from midas.parser.midas import MidasParser @@ -91,7 +91,7 @@ class Checker( Args: block (list[p.Stmt]): the statements to evaluate env (Environment): the environment in which to evaluate - + Returns: bool: whether a return statement is present in the block """ @@ -223,7 +223,7 @@ class Checker( for arg in pos_args + args + kw_args: env.define(arg.name, arg.type) - + returns_hint: Optional[Type] = None if stmt.returns is not None: returns_hint = stmt.returns.accept(self) @@ -417,7 +417,10 @@ class Checker( true_type: Type = expr.if_true.accept(self) false_type: Type = expr.if_false.accept(self) if true_type != false_type: - self.error(expr.location, f"Type mismatch in ternary if branches: true={true_type} != false={false_type}") + self.error( + expr.location, + f"Type mismatch in ternary if branches: true={true_type} != false={false_type}", + ) return UnknownType() return true_type diff --git a/midas/resolver/builtin.py b/midas/resolver/builtin.py index 6a00b14..04bc6e3 100644 --- a/midas/resolver/builtin.py +++ b/midas/resolver/builtin.py @@ -16,6 +16,7 @@ def op(ctx: MidasResolver, t1: Type, operator: str, t2: Type, t3: Type): result=t3, ) + def basic_op(ctx: MidasResolver, type: Type, op: str): ctx.define_operation( left=type, @@ -68,4 +69,4 @@ def define_builtins(ctx: MidasResolver): op(ctx, float, "__gt__", int, bool) # float > int = bool op(ctx, float, "__le__", int, bool) # float <= int = bool op(ctx, float, "__ge__", int, bool) # float >= int = bool - op(ctx, float, "__eq__", int, bool) # float == int = bool \ No newline at end of file + op(ctx, float, "__eq__", int, bool) # float == int = bool -- 2.49.1 From d70137775faf34bba861078d7022993189fe3768 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 12:29:39 +0200 Subject: [PATCH 06/22] feat(cli): update highlighter with new nodes --- midas/cli/highlight.css | 1 + midas/cli/highlighter.py | 71 ++++++++++++++++++++++++---------------- midas/cli/hl_midas.css | 18 +++++----- midas/cli/main.py | 11 +------ 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/midas/cli/highlight.css b/midas/cli/highlight.css index 31f005d..8da787b 100644 --- a/midas/cli/highlight.css +++ b/midas/cli/highlight.css @@ -53,5 +53,6 @@ span { &.keyword { color: rgb(211, 72, 9); + pointer-events: none; } } \ No newline at end of file diff --git a/midas/cli/highlighter.py b/midas/cli/highlighter.py index c0ecea7..690c408 100644 --- a/midas/cli/highlighter.py +++ b/midas/cli/highlighter.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +from dataclasses import dataclass from pathlib import Path from typing import Generic, Optional, Protocol, TextIO, TypeVar @@ -8,6 +9,7 @@ import midas.ast.midas as m import midas.ast.python as p from midas.ast.location import Location from midas.checker.diagnostic import Diagnostic +from midas.lexer.token import Token H = TypeVar("H", bound="Highlighter", contravariant=True) @@ -22,6 +24,15 @@ class Locatable(Protocol): def location(self) -> Optional[Location]: ... +@dataclass(frozen=True) +class LocatableToken: + token: Token + + @property + def location(self) -> Location: + return self.token.get_location() + + class Highlighter(ABC): BASE_CSS_PATH: Path = Path(__file__).parent / "highlight.css" EXTRA_CSS_PATH: Optional[Path] = None @@ -206,34 +217,22 @@ class PythonHighlighter( def visit_ternary_expr(self, expr: p.TernaryExpr) -> None: ... -class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]): +class MidasHighlighter( + Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[None] +): EXTRA_CSS_PATH: Optional[Path] = Path(__file__).parent / "hl_midas.css" def highlight(self, node: Highlightable[MidasHighlighter]): node.accept(self) - def visit_simple_type_stmt(self, stmt: m.SimpleTypeStmt) -> None: - self.wrap(stmt, "simple-type") - if stmt.template is not None: - stmt.template.accept(self) - stmt.base.accept(self) - if stmt.constraint is not None: - self.wrap(stmt.constraint, "constraint") - stmt.constraint.accept(self) - - def visit_complex_type_stmt(self, stmt: m.ComplexTypeStmt) -> None: - self.wrap(stmt, "complex-type") - if stmt.template is not None: - stmt.template.accept(self) - for prop in stmt.properties: - prop.accept(self) + def visit_type_stmt(self, stmt: m.TypeStmt) -> None: + self.wrap(stmt, "type-stmt") + self.wrap(LocatableToken(stmt.name), "type-name") + stmt.type.accept(self) def visit_property_stmt(self, stmt: m.PropertyStmt) -> None: self.wrap(stmt, "property") stmt.type.accept(self) - if stmt.constraint is not None: - self.wrap(stmt.constraint, "constraint") - stmt.constraint.accept(self) def visit_extend_stmt(self, stmt: m.ExtendStmt) -> None: self.wrap(stmt, "extend") @@ -243,17 +242,16 @@ class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]): def visit_op_stmt(self, stmt: m.OpStmt) -> None: self.wrap(stmt, "op") + self.wrap(LocatableToken(stmt.name), "op-name") stmt.operand.accept(self) stmt.result.accept(self) def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None: self.wrap(stmt, "predicate") + self.wrap(LocatableToken(stmt.name), "predicate-name") stmt.type.accept(self) stmt.condition.accept(self) - def visit_simple_type_expr(self, expr: m.SimpleTypeExpr) -> None: - self.wrap(expr, "simple-type-expr") - def visit_logical_expr(self, expr: m.LogicalExpr) -> None: self.wrap(expr, "logical-expr") expr.left.accept(self) @@ -282,14 +280,29 @@ class MidasHighlighter(Highlighter, m.Stmt.Visitor[None], m.Expr.Visitor[None]): def visit_wildcard_expr(self, expr: m.WildcardExpr) -> None: ... - def visit_template_expr(self, expr: m.TemplateExpr) -> None: - self.wrap(expr, "template") - expr.type.accept(self) + def visit_named_type(self, type: m.NamedType) -> None: + self.wrap(type, "named-type") - def visit_type_expr(self, expr: m.TypeExpr) -> None: - self.wrap(expr, "type") - if expr.template is not None: - expr.template.accept(self) + def visit_generic_type(self, type: m.GenericType) -> None: + self.wrap(type, "generic-type") + type.type.accept(self) + for param in type.params: + param.accept(self) + + def visit_constraint_type(self, type: m.ConstraintType) -> None: + self.wrap(type, "constraint-type") + type.type.accept(self) + type.constraint.accept(self) + + def visit_union_type(self, type: m.UnionType) -> None: + self.wrap(type, "union-type") + for type_ in type.types: + type_.accept(self) + + def visit_complex_type(self, type: m.ComplexType) -> None: + self.wrap(type, "complex-type") + for prop in type.properties: + prop.accept(self) class DiagnosticsHighlighter(Highlighter): diff --git a/midas/cli/hl_midas.css b/midas/cli/hl_midas.css index e8adef6..f5b3d1d 100644 --- a/midas/cli/hl_midas.css +++ b/midas/cli/hl_midas.css @@ -5,12 +5,12 @@ span { font-style: italic; } - &.simple-type { - --col: 108, 233, 108; - } - + &.named-type, + &.generic-type, + &.constraint-type, + &.union-type, &.complex-type { - --col: 233, 206, 108; + --col: 150, 150, 150; } &.constraint { @@ -33,10 +33,6 @@ span { --col: 193, 108, 233; } - &.simple-type-expr { - --col: 150, 150, 150; - } - &.logical-expr, &.binary-expr, &.unary-expr, @@ -48,7 +44,9 @@ span { --col: 163, 117, 71; } - &.type { + &.type-name, + &.op-name, + &.predicate-name { --col: 200, 200, 200; font-weight: bold; } diff --git a/midas/cli/main.py b/midas/cli/main.py index e683897..71635d0 100644 --- a/midas/cli/main.py +++ b/midas/cli/main.py @@ -1,7 +1,6 @@ import ast import json import logging -from dataclasses import dataclass from pathlib import Path from typing import Optional, TextIO, get_args @@ -9,7 +8,6 @@ import click import midas.ast.midas as m import midas.ast.python as p -from midas.ast.location import Location from midas.ast.printer import MidasAstPrinter, PythonAstPrinter from midas.checker.checker import Checker from midas.checker.diagnostic import Diagnostic @@ -17,6 +15,7 @@ from midas.checker.types import Type from midas.cli.highlighter import ( DiagnosticsHighlighter, Highlighter, + LocatableToken, MidasHighlighter, PythonHighlighter, ) @@ -142,14 +141,6 @@ def highlight_midas(source: str, path: str) -> Highlighter: for err in parser.errors: print(err.get_report()) - @dataclass(frozen=True) - class LocatableToken: - token: Token - - @property - def location(self) -> Location: - return self.token.get_location() - for stmt in stmts: highlighter.highlight(stmt) for token in tokens: -- 2.49.1 From c8536e20d2d71cb2c5c9ef861dcd61f9655af3fc Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 12:36:21 +0200 Subject: [PATCH 07/22] feat(tests): update Midas serializer --- tests/serializer/midas.py | 83 +++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/tests/serializer/midas.py b/tests/serializer/midas.py index 0991334..6da0dec 100644 --- a/tests/serializer/midas.py +++ b/tests/serializer/midas.py @@ -2,56 +2,61 @@ from typing import Optional, Sequence from midas.ast.midas import ( BinaryExpr, - ComplexTypeStmt, + ComplexType, + ConstraintType, Expr, ExtendStmt, + GenericType, GetExpr, GroupingExpr, LiteralExpr, LogicalExpr, + NamedType, OpStmt, PredicateStmt, PropertyStmt, - SimpleTypeExpr, - SimpleTypeStmt, Stmt, - TemplateExpr, - TypeExpr, + Type, + TypeStmt, UnaryExpr, + UnionType, VariableExpr, WildcardExpr, ) -class MidasAstJsonSerializer(Stmt.Visitor[dict], Expr.Visitor[dict]): +class MidasAstJsonSerializer( + Stmt.Visitor[dict], Expr.Visitor[dict], Type.Visitor[dict] +): """An AST serializer which produces a JSON-compatible structure""" def serialize(self, stmts: list[Stmt]) -> list[dict]: return [stmt.accept(self) for stmt in stmts] - def _serialize_optional(self, element: Optional[Stmt | Expr]) -> Optional[dict]: + def _serialize_optional( + self, element: Optional[Stmt | Expr | Type] + ) -> Optional[dict]: if element is None: return None return element.accept(self) - def _serialize_list(self, elements: Sequence[Stmt | Expr]) -> list[dict]: + def _serialize_list(self, elements: Sequence[Stmt | Expr | Type]) -> list[dict]: return [element.accept(self) for element in elements] - def visit_simple_type_stmt(self, stmt: SimpleTypeStmt) -> dict: + def visit_type_stmt(self, stmt: TypeStmt) -> dict: return { - "_type": "SimpleTypeStmt", + "_type": "TypeStmt", "name": stmt.name.lexeme, - "template": self._serialize_optional(stmt.template), - "base": stmt.base.accept(self), - "constraint": self._serialize_optional(stmt.constraint), + "params": [ + self._serialize_type_stmt_template_param(param) for param in stmt.params + ], + "type": stmt.type.accept(self), } - def visit_complex_type_stmt(self, stmt: ComplexTypeStmt) -> dict: + def _serialize_type_stmt_template_param(self, param: TypeStmt.Param) -> dict: return { - "_type": "ComplexTypeStmt", - "name": stmt.name.lexeme, - "template": self._serialize_optional(stmt.template), - "properties": self._serialize_list(stmt.properties), + "name": param.name.lexeme, + "bound": self._serialize_optional(param.bound), } def visit_property_stmt(self, stmt: PropertyStmt) -> dict: @@ -59,7 +64,6 @@ class MidasAstJsonSerializer(Stmt.Visitor[dict], Expr.Visitor[dict]): "_type": "PropertyStmt", "name": stmt.name.lexeme, "type": stmt.type.accept(self), - "constraint": self._serialize_optional(stmt.constraint), } def visit_extend_stmt(self, stmt: ExtendStmt) -> dict: @@ -86,13 +90,6 @@ class MidasAstJsonSerializer(Stmt.Visitor[dict], Expr.Visitor[dict]): "condition": stmt.condition.accept(self), } - def visit_simple_type_expr(self, expr: SimpleTypeExpr) -> dict: - return { - "_type": "SimpleTypeExpr", - "name": expr.name.lexeme, - "optional": expr.optional, - } - def visit_logical_expr(self, expr: LogicalExpr) -> dict: return { "_type": "LogicalExpr", @@ -144,16 +141,34 @@ class MidasAstJsonSerializer(Stmt.Visitor[dict], Expr.Visitor[dict]): def visit_wildcard_expr(self, expr: WildcardExpr) -> dict: return {"_type": "WildcardExpr"} - def visit_template_expr(self, expr: TemplateExpr) -> dict: + def visit_named_type(self, type: NamedType) -> dict: return { - "_type": "TemplateExpr", - "type": expr.type.accept(self), + "_type": "NamedType", + "name": type.name.lexeme, } - def visit_type_expr(self, expr: TypeExpr) -> dict: + def visit_generic_type(self, type: GenericType) -> dict: return { - "_type": "TypeExpr", - "name": expr.name.lexeme, - "template": self._serialize_optional(expr.template), - "optional": expr.optional, + "_type": "GenericType", + "type": type.type.accept(self), + "params": self._serialize_list(type.params), + } + + def visit_constraint_type(self, type: ConstraintType) -> dict: + return { + "_type": "ConstraintType", + "type": type.type.accept(self), + "constraint": type.constraint.accept(self), + } + + def visit_union_type(self, type: UnionType) -> dict: + return { + "_type": "UnionType", + "types": self._serialize_list(type.types), + } + + def visit_complex_type(self, type: ComplexType) -> dict: + return { + "_type": "ComplexType", + "properties": self._serialize_list(type.properties), } -- 2.49.1 From dee479def512f1aa5e516f4fd868c8882d159fbe Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 13:00:03 +0200 Subject: [PATCH 08/22] fix(checker): wrap type definitions in AliasType --- midas/resolver/midas.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/midas/resolver/midas.py b/midas/resolver/midas.py index ff97dbc..9ffce85 100644 --- a/midas/resolver/midas.py +++ b/midas/resolver/midas.py @@ -2,6 +2,7 @@ from typing import Optional import midas.ast.midas as m from midas.checker.types import ( + AliasType, Type, UnionType, UnknownType, @@ -103,7 +104,8 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T for param in stmt.params: if param.bound is not None: param.bound.accept(self) - self.define_type(stmt.name.lexeme, type) + name: str = stmt.name.lexeme + self.define_type(name, AliasType(name=name, type=type)) def visit_property_stmt(self, stmt: m.PropertyStmt) -> None: ... -- 2.49.1 From 97b1ee8ab8e359c219de52070aff89c4e3b8155f Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 13:00:43 +0200 Subject: [PATCH 09/22] feat(cli): add format command --- midas/ast/printer.py | 6 +++--- midas/cli/main.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/midas/ast/printer.py b/midas/ast/printer.py index ed9e069..ee59279 100644 --- a/midas/ast/printer.py +++ b/midas/ast/printer.py @@ -283,7 +283,7 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str] def indented(self, text: str) -> str: return " " * (self.level * self.indent) + text - def print(self, expr: m.Expr | m.Stmt): + def print(self, expr: m.Expr | m.Stmt | m.Type) -> str: self.level = 0 return expr.accept(self) @@ -314,13 +314,13 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str] for op in stmt.operations: res += op.accept(self) self.level -= 1 - res += "\n" + self.indented("}") + res += self.indented("}") return res def visit_op_stmt(self, stmt: m.OpStmt): operand: str = stmt.operand.accept(self) result: str = stmt.result.accept(self) - return self.indented(f"op {stmt.name.lexeme}({operand}) -> {result}") + return self.indented(f"op {stmt.name.lexeme}({operand}) -> {result}\n") def visit_predicate_stmt(self, stmt: m.PredicateStmt): name: str = stmt.name.lexeme diff --git a/midas/cli/main.py b/midas/cli/main.py index 71635d0..a9833bb 100644 --- a/midas/cli/main.py +++ b/midas/cli/main.py @@ -8,7 +8,7 @@ import click import midas.ast.midas as m import midas.ast.python as p -from midas.ast.printer import MidasAstPrinter, PythonAstPrinter +from midas.ast.printer import MidasAstPrinter, MidasPrinter, PythonAstPrinter from midas.checker.checker import Checker from midas.checker.diagnostic import Diagnostic from midas.checker.types import Type @@ -167,5 +167,21 @@ def highlight(output: TextIO, file: TextIO): highlighter.dump(output) +@midas.command() +@click.option("-o", "--output", type=click.File("w"), default="-") +@click.argument("file", type=click.File("r")) +def format(output: TextIO, file: TextIO): + source: str = file.read() + printer = MidasPrinter() + lexer = MidasLexer(source, file=file.name) + tokens: list[Token] = lexer.process() + parser = MidasParser(tokens) + stmts: list[m.Stmt] = parser.parse() + for err in parser.errors: + print(err.get_report()) + for stmt in stmts: + output.write(printer.print(stmt) + "\n") + + if __name__ == "__main__": midas() -- 2.49.1 From 2fd2071d40b5d4e3c62ed29c450d160c04ebf436 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 13:02:45 +0200 Subject: [PATCH 10/22] feat(parser): parse pass statement and None --- midas/parser/python.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/midas/parser/python.py b/midas/parser/python.py index 265e8be..79011bc 100644 --- a/midas/parser/python.py +++ b/midas/parser/python.py @@ -87,6 +87,9 @@ class PythonParser: case ast.If(): return self.parse_if(node) + case ast.Pass(): + return None + case _: print(f"Unsupported statement: {ast.unparse(node)}") return None @@ -311,6 +314,13 @@ class PythonParser: constraint=right_expr, ) + case ast.Constant(value=None): + return BaseType( + location=loc, + base="None", + param=None, + ) + case _: raise UnsupportedSyntaxError(type_expr) -- 2.49.1 From a4139d465246e2003c40517af9c51f095bb1f67a Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 13:03:07 +0200 Subject: [PATCH 11/22] feat(checker): handle logical expressions --- midas/checker/checker.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index c210b7c..82768ca 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -398,7 +398,16 @@ class Checker( def visit_variable_expr(self, expr: p.VariableExpr) -> Type: return self.look_up_variable(expr.name, expr) or UnknownType() - def visit_logical_expr(self, expr: p.LogicalExpr) -> Type: ... + def visit_logical_expr(self, expr: p.LogicalExpr) -> Type: + left: Type = expr.left.accept(self) + right: Type = expr.right.accept(self) + # TODO: union type + if left != right: + self.error( + expr.location, + f"Operands must be of the same type, left={left} != right={right}", + ) + return left def visit_set_expr(self, expr: p.SetExpr) -> Type: ... -- 2.49.1 From 1c5c418f1c6689083afa1357faf1ba95def31d06 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 13:05:06 +0200 Subject: [PATCH 12/22] fix(tests): serialize ternary expressions --- tests/serializer/python.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/serializer/python.py b/tests/serializer/python.py index 8dc43f4..786b15b 100644 --- a/tests/serializer/python.py +++ b/tests/serializer/python.py @@ -22,6 +22,7 @@ from midas.ast.python import ( ReturnStmt, SetExpr, Stmt, + TernaryExpr, TypeAssign, UnaryExpr, VariableExpr, @@ -245,3 +246,11 @@ class PythonAstJsonSerializer( "type": expr.type.accept(self), "expr": expr.expr.accept(self), } + + def visit_ternary_expr(self, expr: TernaryExpr) -> dict: + return { + "_type": "TernaryExpr", + "test": expr.test.accept(self), + "if_true": expr.if_true.accept(self), + "if_false": expr.if_false.accept(self), + } -- 2.49.1 From 029caf4526a30835cae7f498b8e621d48410cb4e Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 13:05:38 +0200 Subject: [PATCH 13/22] fix(tests): update tests with new syntax --- tests/cases/checker/04_custom_types.midas | 6 +- .../cases/midas-parser/01_simple_types.midas | 16 +- .../01_simple_types.midas.ref.json | 841 ++++++++++-------- 3 files changed, 462 insertions(+), 401 deletions(-) diff --git a/tests/cases/checker/04_custom_types.midas b/tests/cases/checker/04_custom_types.midas index eee3eb9..6a1a6a2 100644 --- a/tests/cases/checker/04_custom_types.midas +++ b/tests/cases/checker/04_custom_types.midas @@ -1,6 +1,6 @@ -type Meter(float) -type Second(float) -type MeterPerSecond(float) +type Meter = float +type Second = float +type MeterPerSecond = float extend Meter { op __add__(Meter) -> Meter diff --git a/tests/cases/midas-parser/01_simple_types.midas b/tests/cases/midas-parser/01_simple_types.midas index 9432751..a6c493d 100644 --- a/tests/cases/midas-parser/01_simple_types.midas +++ b/tests/cases/midas-parser/01_simple_types.midas @@ -1,15 +1,15 @@ // Simple custom type derived from float -type Custom(float) +type Custom = float // Simple custom types with constraints -type Latitude(float) where (-90 <= _ <= 90) -type Longitude(float) where (-180 <= _ <= 180) +type Latitude = float where (-90 <= _ <= 90) +type Longitude = float where (-180 <= _ <= 180) // Generic custom type (a Difference of T is derived from T, e.g. a difference of floats is a float -type Difference[T](T) +type Difference[T] = T // Complex custom type, containing two values accessible through properties -type GeoLocation { +type GeoLocation = { lat: Latitude lon: Longitude } @@ -24,7 +24,7 @@ extend GeoLocation { // For complex generics, you need to specify how the genericity the properties // are handled -type Difference[GeoLocation] { +type Difference[GeoLocation] = { lat: Difference[Latitude] lon: Difference[Longitude] } @@ -44,11 +44,11 @@ predicate StrictlyPositive(v: float) = v > 0 predicate Equatorial(loc: GeoLocation) = (-10 <= loc.lat <= 10) predicate Arctic(loc: GeoLocation) = (loc.lat >= 66) -type Person { +type Person = { name: str // Property with an inline constraint - age: int? where (0 <= _ < 150) + age: None | (int where (0 <= _ < 150)) // Property referencing a predicate height: float where StrictlyPositive diff --git a/tests/cases/midas-parser/01_simple_types.midas.ref.json b/tests/cases/midas-parser/01_simple_types.midas.ref.json index 355b80e..5d70b51 100644 --- a/tests/cases/midas-parser/01_simple_types.midas.ref.json +++ b/tests/cases/midas-parser/01_simple_types.midas.ref.json @@ -31,28 +31,34 @@ "column": 6 }, { - "type": "LEFT_PAREN", - "lexeme": "(", + "type": "WHITESPACE", + "lexeme": " ", "line": 2, "column": 12 }, + { + "type": "EQUAL", + "lexeme": "=", + "line": 2, + "column": 13 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 2, + "column": 14 + }, { "type": "IDENTIFIER", "lexeme": "float", "line": 2, - "column": 13 - }, - { - "type": "RIGHT_PAREN", - "lexeme": ")", - "line": 2, - "column": 18 + "column": 15 }, { "type": "NEWLINE", "lexeme": "\n", "line": 2, - "column": 19 + "column": 20 }, { "type": "NEWLINE", @@ -91,118 +97,124 @@ "column": 6 }, { - "type": "LEFT_PAREN", - "lexeme": "(", + "type": "WHITESPACE", + "lexeme": " ", "line": 5, "column": 14 }, + { + "type": "EQUAL", + "lexeme": "=", + "line": 5, + "column": 15 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 5, + "column": 16 + }, { "type": "IDENTIFIER", "lexeme": "float", "line": 5, - "column": 15 - }, - { - "type": "RIGHT_PAREN", - "lexeme": ")", - "line": 5, - "column": 20 + "column": 17 }, { "type": "WHITESPACE", "lexeme": " ", "line": 5, - "column": 21 + "column": 22 }, { "type": "WHERE", "lexeme": "where", "line": 5, - "column": 22 + "column": 23 }, { "type": "WHITESPACE", "lexeme": " ", "line": 5, - "column": 27 + "column": 28 }, { "type": "LEFT_PAREN", "lexeme": "(", "line": 5, - "column": 28 + "column": 29 }, { "type": "MINUS", "lexeme": "-", "line": 5, - "column": 29 + "column": 30 }, { "type": "NUMBER", "lexeme": "90", "line": 5, - "column": 30 + "column": 31 }, { "type": "WHITESPACE", "lexeme": " ", "line": 5, - "column": 32 + "column": 33 }, { "type": "LESS_EQUAL", "lexeme": "<=", "line": 5, - "column": 33 + "column": 34 }, { "type": "WHITESPACE", "lexeme": " ", "line": 5, - "column": 35 + "column": 36 }, { "type": "UNDERSCORE", "lexeme": "_", "line": 5, - "column": 36 + "column": 37 }, { "type": "WHITESPACE", "lexeme": " ", "line": 5, - "column": 37 + "column": 38 }, { "type": "LESS_EQUAL", "lexeme": "<=", "line": 5, - "column": 38 + "column": 39 }, { "type": "WHITESPACE", "lexeme": " ", "line": 5, - "column": 40 + "column": 41 }, { "type": "NUMBER", "lexeme": "90", "line": 5, - "column": 41 + "column": 42 }, { "type": "RIGHT_PAREN", "lexeme": ")", "line": 5, - "column": 43 + "column": 44 }, { "type": "NEWLINE", "lexeme": "\n", "line": 5, - "column": 44 + "column": 45 }, { "type": "TYPE", @@ -223,118 +235,124 @@ "column": 6 }, { - "type": "LEFT_PAREN", - "lexeme": "(", + "type": "WHITESPACE", + "lexeme": " ", "line": 6, "column": 15 }, + { + "type": "EQUAL", + "lexeme": "=", + "line": 6, + "column": 16 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 6, + "column": 17 + }, { "type": "IDENTIFIER", "lexeme": "float", "line": 6, - "column": 16 - }, - { - "type": "RIGHT_PAREN", - "lexeme": ")", - "line": 6, - "column": 21 + "column": 18 }, { "type": "WHITESPACE", "lexeme": " ", "line": 6, - "column": 22 + "column": 23 }, { "type": "WHERE", "lexeme": "where", "line": 6, - "column": 23 + "column": 24 }, { "type": "WHITESPACE", "lexeme": " ", "line": 6, - "column": 28 + "column": 29 }, { "type": "LEFT_PAREN", "lexeme": "(", "line": 6, - "column": 29 + "column": 30 }, { "type": "MINUS", "lexeme": "-", "line": 6, - "column": 30 + "column": 31 }, { "type": "NUMBER", "lexeme": "180", "line": 6, - "column": 31 + "column": 32 }, { "type": "WHITESPACE", "lexeme": " ", "line": 6, - "column": 34 + "column": 35 }, { "type": "LESS_EQUAL", "lexeme": "<=", "line": 6, - "column": 35 + "column": 36 }, { "type": "WHITESPACE", "lexeme": " ", "line": 6, - "column": 37 + "column": 38 }, { "type": "UNDERSCORE", "lexeme": "_", "line": 6, - "column": 38 + "column": 39 }, { "type": "WHITESPACE", "lexeme": " ", "line": 6, - "column": 39 + "column": 40 }, { "type": "LESS_EQUAL", "lexeme": "<=", "line": 6, - "column": 40 + "column": 41 }, { "type": "WHITESPACE", "lexeme": " ", "line": 6, - "column": 42 + "column": 43 }, { "type": "NUMBER", "lexeme": "180", "line": 6, - "column": 43 + "column": 44 }, { "type": "RIGHT_PAREN", "lexeme": ")", "line": 6, - "column": 46 + "column": 47 }, { "type": "NEWLINE", "lexeme": "\n", "line": 6, - "column": 47 + "column": 48 }, { "type": "NEWLINE", @@ -391,28 +409,34 @@ "column": 18 }, { - "type": "LEFT_PAREN", - "lexeme": "(", + "type": "WHITESPACE", + "lexeme": " ", "line": 9, "column": 19 }, + { + "type": "EQUAL", + "lexeme": "=", + "line": 9, + "column": 20 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 9, + "column": 21 + }, { "type": "IDENTIFIER", "lexeme": "T", "line": 9, - "column": 20 - }, - { - "type": "RIGHT_PAREN", - "lexeme": ")", - "line": 9, - "column": 21 + "column": 22 }, { "type": "NEWLINE", "lexeme": "\n", "line": 9, - "column": 22 + "column": 23 }, { "type": "NEWLINE", @@ -456,17 +480,29 @@ "line": 12, "column": 17 }, + { + "type": "EQUAL", + "lexeme": "=", + "line": 12, + "column": 18 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 12, + "column": 19 + }, { "type": "LEFT_BRACE", "lexeme": "{", "line": 12, - "column": 18 + "column": 20 }, { "type": "NEWLINE", "lexeme": "\n", "line": 12, - "column": 19 + "column": 21 }, { "type": "WHITESPACE", @@ -834,17 +870,29 @@ "line": 27, "column": 29 }, + { + "type": "EQUAL", + "lexeme": "=", + "line": 27, + "column": 30 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 27, + "column": 31 + }, { "type": "LEFT_BRACE", "lexeme": "{", "line": 27, - "column": 30 + "column": 32 }, { "type": "NEWLINE", "lexeme": "\n", "line": 27, - "column": 31 + "column": 33 }, { "type": "WHITESPACE", @@ -1824,17 +1872,29 @@ "line": 47, "column": 12 }, + { + "type": "EQUAL", + "lexeme": "=", + "line": 47, + "column": 13 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 47, + "column": 14 + }, { "type": "LEFT_BRACE", "lexeme": "{", "line": 47, - "column": 13 + "column": 15 }, { "type": "NEWLINE", "lexeme": "\n", "line": 47, - "column": 14 + "column": 16 }, { "type": "WHITESPACE", @@ -1922,16 +1982,10 @@ }, { "type": "IDENTIFIER", - "lexeme": "int", + "lexeme": "None", "line": 51, "column": 10 }, - { - "type": "QMARK", - "lexeme": "?", - "line": 51, - "column": 13 - }, { "type": "WHITESPACE", "lexeme": " ", @@ -1939,8 +1993,8 @@ "column": 14 }, { - "type": "WHERE", - "lexeme": "where", + "type": "PIPE", + "lexeme": "|", "line": 51, "column": 15 }, @@ -1948,17 +2002,29 @@ "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 20 + "column": 16 }, { "type": "LEFT_PAREN", "lexeme": "(", "line": 51, + "column": 17 + }, + { + "type": "IDENTIFIER", + "lexeme": "int", + "line": 51, + "column": 18 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 51, "column": 21 }, { - "type": "NUMBER", - "lexeme": "0", + "type": "WHERE", + "lexeme": "where", "line": 51, "column": 22 }, @@ -1966,35 +2032,17 @@ "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 23 - }, - { - "type": "LESS_EQUAL", - "lexeme": "<=", - "line": 51, - "column": 24 - }, - { - "type": "WHITESPACE", - "lexeme": " ", - "line": 51, - "column": 26 - }, - { - "type": "UNDERSCORE", - "lexeme": "_", - "line": 51, "column": 27 }, { - "type": "WHITESPACE", - "lexeme": " ", + "type": "LEFT_PAREN", + "lexeme": "(", "line": 51, "column": 28 }, { - "type": "LESS", - "lexeme": "<", + "type": "NUMBER", + "lexeme": "0", "line": 51, "column": 29 }, @@ -2004,23 +2052,65 @@ "line": 51, "column": 30 }, + { + "type": "LESS_EQUAL", + "lexeme": "<=", + "line": 51, + "column": 31 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 51, + "column": 33 + }, + { + "type": "UNDERSCORE", + "lexeme": "_", + "line": 51, + "column": 34 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 51, + "column": 35 + }, + { + "type": "LESS", + "lexeme": "<", + "line": 51, + "column": 36 + }, + { + "type": "WHITESPACE", + "lexeme": " ", + "line": 51, + "column": 37 + }, { "type": "NUMBER", "lexeme": "150", "line": 51, - "column": 31 + "column": 38 }, { "type": "RIGHT_PAREN", "lexeme": ")", "line": 51, - "column": 34 + "column": 41 + }, + { + "type": "RIGHT_PAREN", + "lexeme": ")", + "line": 51, + "column": 42 }, { "type": "NEWLINE", "lexeme": "\n", "line": 51, - "column": 35 + "column": 43 }, { "type": "NEWLINE", @@ -2169,259 +2259,235 @@ ], "stmts": [ { - "_type": "SimpleTypeStmt", + "_type": "TypeStmt", "name": "Custom", - "template": null, - "base": { - "_type": "TypeExpr", - "name": "float", - "template": null, - "optional": false - }, - "constraint": null + "params": [], + "type": { + "_type": "NamedType", + "name": "float" + } }, { - "_type": "SimpleTypeStmt", + "_type": "TypeStmt", "name": "Latitude", - "template": null, - "base": { - "_type": "TypeExpr", - "name": "float", - "template": null, - "optional": false - }, - "constraint": { - "_type": "GroupingExpr", - "expr": { - "_type": "BinaryExpr", - "left": { + "params": [], + "type": { + "_type": "ConstraintType", + "type": { + "_type": "NamedType", + "name": "float" + }, + "constraint": { + "_type": "GroupingExpr", + "expr": { "_type": "BinaryExpr", "left": { - "_type": "UnaryExpr", - "operator": "-", + "_type": "BinaryExpr", + "left": { + "_type": "UnaryExpr", + "operator": "-", + "right": { + "_type": "LiteralExpr", + "value": 90.0 + } + }, + "operator": "<=", "right": { - "_type": "LiteralExpr", - "value": 90.0 + "_type": "WildcardExpr" } }, "operator": "<=", "right": { - "_type": "WildcardExpr" + "_type": "LiteralExpr", + "value": 90.0 } - }, - "operator": "<=", - "right": { - "_type": "LiteralExpr", - "value": 90.0 } } } }, { - "_type": "SimpleTypeStmt", + "_type": "TypeStmt", "name": "Longitude", - "template": null, - "base": { - "_type": "TypeExpr", - "name": "float", - "template": null, - "optional": false - }, - "constraint": { - "_type": "GroupingExpr", - "expr": { - "_type": "BinaryExpr", - "left": { + "params": [], + "type": { + "_type": "ConstraintType", + "type": { + "_type": "NamedType", + "name": "float" + }, + "constraint": { + "_type": "GroupingExpr", + "expr": { "_type": "BinaryExpr", "left": { - "_type": "UnaryExpr", - "operator": "-", + "_type": "BinaryExpr", + "left": { + "_type": "UnaryExpr", + "operator": "-", + "right": { + "_type": "LiteralExpr", + "value": 180.0 + } + }, + "operator": "<=", "right": { - "_type": "LiteralExpr", - "value": 180.0 + "_type": "WildcardExpr" } }, "operator": "<=", "right": { - "_type": "WildcardExpr" + "_type": "LiteralExpr", + "value": 180.0 } - }, - "operator": "<=", - "right": { - "_type": "LiteralExpr", - "value": 180.0 } } } }, { - "_type": "SimpleTypeStmt", + "_type": "TypeStmt", "name": "Difference", - "template": { - "_type": "TemplateExpr", - "type": { - "_type": "TypeExpr", + "params": [ + { "name": "T", - "template": null, - "optional": false + "bound": null } - }, - "base": { - "_type": "TypeExpr", - "name": "T", - "template": null, - "optional": false - }, - "constraint": null + ], + "type": { + "_type": "NamedType", + "name": "T" + } }, { - "_type": "ComplexTypeStmt", + "_type": "TypeStmt", "name": "GeoLocation", - "template": null, - "properties": [ - { - "_type": "PropertyStmt", - "name": "lat", - "type": { - "_type": "TypeExpr", - "name": "Latitude", - "template": null, - "optional": false + "params": [], + "type": { + "_type": "ComplexType", + "properties": [ + { + "_type": "PropertyStmt", + "name": "lat", + "type": { + "_type": "NamedType", + "name": "Latitude" + } }, - "constraint": null - }, - { - "_type": "PropertyStmt", - "name": "lon", - "type": { - "_type": "TypeExpr", - "name": "Longitude", - "template": null, - "optional": false - }, - "constraint": null - } - ] + { + "_type": "PropertyStmt", + "name": "lon", + "type": { + "_type": "NamedType", + "name": "Longitude" + } + } + ] + } }, { "_type": "ExtendStmt", "type": { - "_type": "TypeExpr", - "name": "GeoLocation", - "template": null, - "optional": false + "_type": "NamedType", + "name": "GeoLocation" }, "operations": [ { "_type": "OpStmt", "name": "__sub__", "operand": { - "_type": "TypeExpr", - "name": "GeoLocation", - "template": null, - "optional": false + "_type": "NamedType", + "name": "GeoLocation" }, "result": { - "_type": "TypeExpr", - "name": "Difference", - "template": { - "_type": "TemplateExpr", - "type": { - "_type": "TypeExpr", - "name": "GeoLocation", - "template": null, - "optional": false - } + "_type": "GenericType", + "type": { + "_type": "NamedType", + "name": "Difference" }, - "optional": false + "params": [ + { + "_type": "NamedType", + "name": "GeoLocation" + } + ] } } ] }, { - "_type": "ComplexTypeStmt", + "_type": "TypeStmt", "name": "Difference", - "template": { - "_type": "TemplateExpr", - "type": { - "_type": "TypeExpr", + "params": [ + { "name": "GeoLocation", - "template": null, - "optional": false + "bound": null } - }, - "properties": [ - { - "_type": "PropertyStmt", - "name": "lat", - "type": { - "_type": "TypeExpr", - "name": "Difference", - "template": { - "_type": "TemplateExpr", + ], + "type": { + "_type": "ComplexType", + "properties": [ + { + "_type": "PropertyStmt", + "name": "lat", + "type": { + "_type": "GenericType", "type": { - "_type": "TypeExpr", - "name": "Latitude", - "template": null, - "optional": false - } - }, - "optional": false + "_type": "NamedType", + "name": "Difference" + }, + "params": [ + { + "_type": "NamedType", + "name": "Latitude" + } + ] + } }, - "constraint": null - }, - { - "_type": "PropertyStmt", - "name": "lon", - "type": { - "_type": "TypeExpr", - "name": "Difference", - "template": { - "_type": "TemplateExpr", + { + "_type": "PropertyStmt", + "name": "lon", + "type": { + "_type": "GenericType", "type": { - "_type": "TypeExpr", - "name": "Longitude", - "template": null, - "optional": false - } - }, - "optional": false - }, - "constraint": null - } - ] + "_type": "NamedType", + "name": "Difference" + }, + "params": [ + { + "_type": "NamedType", + "name": "Longitude" + } + ] + } + } + ] + } }, { "_type": "ExtendStmt", "type": { - "_type": "TypeExpr", - "name": "Latitude", - "template": null, - "optional": false + "_type": "NamedType", + "name": "Latitude" }, "operations": [ { "_type": "OpStmt", "name": "__sub__", "operand": { - "_type": "TypeExpr", - "name": "Latitude", - "template": null, - "optional": false + "_type": "NamedType", + "name": "Latitude" }, "result": { - "_type": "TypeExpr", - "name": "Difference", - "template": { - "_type": "TemplateExpr", - "type": { - "_type": "TypeExpr", - "name": "Latitude", - "template": null, - "optional": false - } + "_type": "GenericType", + "type": { + "_type": "NamedType", + "name": "Difference" }, - "optional": false + "params": [ + { + "_type": "NamedType", + "name": "Latitude" + } + ] } } ] @@ -2429,34 +2495,29 @@ { "_type": "ExtendStmt", "type": { - "_type": "TypeExpr", - "name": "Longitude", - "template": null, - "optional": false + "_type": "NamedType", + "name": "Longitude" }, "operations": [ { "_type": "OpStmt", "name": "__sub__", "operand": { - "_type": "TypeExpr", - "name": "Longitude", - "template": null, - "optional": false + "_type": "NamedType", + "name": "Longitude" }, "result": { - "_type": "TypeExpr", - "name": "Difference", - "template": { - "_type": "TemplateExpr", - "type": { - "_type": "TypeExpr", - "name": "Longitude", - "template": null, - "optional": false - } + "_type": "GenericType", + "type": { + "_type": "NamedType", + "name": "Difference" }, - "optional": false + "params": [ + { + "_type": "NamedType", + "name": "Longitude" + } + ] } } ] @@ -2466,10 +2527,8 @@ "name": "Positive", "subject": "v", "type": { - "_type": "TypeExpr", - "name": "float", - "template": null, - "optional": false + "_type": "NamedType", + "name": "float" }, "condition": { "_type": "BinaryExpr", @@ -2489,10 +2548,8 @@ "name": "StrictlyPositive", "subject": "v", "type": { - "_type": "TypeExpr", - "name": "float", - "template": null, - "optional": false + "_type": "NamedType", + "name": "float" }, "condition": { "_type": "BinaryExpr", @@ -2512,10 +2569,8 @@ "name": "Equatorial", "subject": "loc", "type": { - "_type": "TypeExpr", - "name": "GeoLocation", - "template": null, - "optional": false + "_type": "NamedType", + "name": "GeoLocation" }, "condition": { "_type": "GroupingExpr", @@ -2554,10 +2609,8 @@ "name": "Arctic", "subject": "loc", "type": { - "_type": "TypeExpr", - "name": "GeoLocation", - "template": null, - "optional": false + "_type": "NamedType", + "name": "GeoLocation" }, "condition": { "_type": "GroupingExpr", @@ -2580,79 +2633,87 @@ } }, { - "_type": "ComplexTypeStmt", + "_type": "TypeStmt", "name": "Person", - "template": null, - "properties": [ - { - "_type": "PropertyStmt", - "name": "name", - "type": { - "_type": "TypeExpr", - "name": "str", - "template": null, - "optional": false + "params": [], + "type": { + "_type": "ComplexType", + "properties": [ + { + "_type": "PropertyStmt", + "name": "name", + "type": { + "_type": "NamedType", + "name": "str" + } }, - "constraint": null - }, - { - "_type": "PropertyStmt", - "name": "age", - "type": { - "_type": "TypeExpr", - "name": "int", - "template": null, - "optional": true - }, - "constraint": { - "_type": "GroupingExpr", - "expr": { - "_type": "BinaryExpr", - "left": { - "_type": "BinaryExpr", - "left": { - "_type": "LiteralExpr", - "value": 0.0 + { + "_type": "PropertyStmt", + "name": "age", + "type": { + "_type": "UnionType", + "types": [ + { + "_type": "NamedType", + "name": "None" }, - "operator": "<=", - "right": { - "_type": "WildcardExpr" + { + "_type": "ConstraintType", + "type": { + "_type": "NamedType", + "name": "int" + }, + "constraint": { + "_type": "GroupingExpr", + "expr": { + "_type": "BinaryExpr", + "left": { + "_type": "BinaryExpr", + "left": { + "_type": "LiteralExpr", + "value": 0.0 + }, + "operator": "<=", + "right": { + "_type": "WildcardExpr" + } + }, + "operator": "<", + "right": { + "_type": "LiteralExpr", + "value": 150.0 + } + } + } } + ] + } + }, + { + "_type": "PropertyStmt", + "name": "height", + "type": { + "_type": "ConstraintType", + "type": { + "_type": "NamedType", + "name": "float" }, - "operator": "<", - "right": { - "_type": "LiteralExpr", - "value": 150.0 + "constraint": { + "_type": "VariableExpr", + "name": "StrictlyPositive" } } - } - }, - { - "_type": "PropertyStmt", - "name": "height", - "type": { - "_type": "TypeExpr", - "name": "float", - "template": null, - "optional": false }, - "constraint": { - "_type": "VariableExpr", - "name": "StrictlyPositive" + { + "_type": "PropertyStmt", + "name": "home", + "type": { + "_type": "NamedType", + "name": "GeoLocation" + } } - }, - { - "_type": "PropertyStmt", - "name": "home", - "type": { - "_type": "TypeExpr", - "name": "GeoLocation", - "template": null, - "optional": false - }, - "constraint": null - } - ] + ] + } } ], "errors": [] -- 2.49.1 From 63a43d79dd044a1126da2541d8d80171a33dd81c Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 13:07:53 +0200 Subject: [PATCH 14/22] chore: update examples --- examples/01_simple_type_checking/02_simple_types.midas | 6 +++--- examples/01_simple_type_checking/03_control_flow.py | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/01_simple_type_checking/02_simple_types.midas b/examples/01_simple_type_checking/02_simple_types.midas index eee3eb9..6a1a6a2 100644 --- a/examples/01_simple_type_checking/02_simple_types.midas +++ b/examples/01_simple_type_checking/02_simple_types.midas @@ -1,6 +1,6 @@ -type Meter(float) -type Second(float) -type MeterPerSecond(float) +type Meter = float +type Second = float +type MeterPerSecond = float extend Meter { op __add__(Meter) -> Meter diff --git a/examples/01_simple_type_checking/03_control_flow.py b/examples/01_simple_type_checking/03_control_flow.py index 90f5530..772c9ac 100644 --- a/examples/01_simple_type_checking/03_control_flow.py +++ b/examples/01_simple_type_checking/03_control_flow.py @@ -4,13 +4,20 @@ def minimum(x: int, y: int): else: return y + a = 15 b = 72 c = minimum(a, b) + def factorial(n: int) -> int: if n <= 1: return 1 return n * factorial(n - 1) -category = "Category 1" if a < 10 else "Category 2" \ No newline at end of file + +category = "Category 1" if a < 10 else "Category 2" + + +def foo() -> None: + pass -- 2.49.1 From 828ec9a3faa202cf31c043dc955cc4ffd82d8601 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 17:19:17 +0200 Subject: [PATCH 15/22] fix!: remove union type --- gen/midas.py | 4 ---- midas/ast/midas.py | 11 ----------- midas/ast/printer.py | 15 --------------- midas/checker/types.py | 9 +-------- midas/cli/highlighter.py | 5 ----- midas/cli/hl_midas.css | 1 - midas/lexer/midas.py | 2 -- midas/lexer/token.py | 1 - midas/parser/midas.py | 14 +------------- midas/resolver/midas.py | 5 ----- tests/serializer/midas.py | 7 ------- 11 files changed, 2 insertions(+), 72 deletions(-) diff --git a/gen/midas.py b/gen/midas.py index dc0efd0..e1c304d 100644 --- a/gen/midas.py +++ b/gen/midas.py @@ -111,10 +111,6 @@ class ConstraintType: constraint: Expr -class UnionType: - types: list[Type] - - class ComplexType: properties: list[PropertyStmt] diff --git a/midas/ast/midas.py b/midas/ast/midas.py index a18b460..335e5cf 100644 --- a/midas/ast/midas.py +++ b/midas/ast/midas.py @@ -228,9 +228,6 @@ class Type(ABC): @abstractmethod def visit_constraint_type(self, type: ConstraintType) -> T: ... - @abstractmethod - def visit_union_type(self, type: UnionType) -> T: ... - @abstractmethod def visit_complex_type(self, type: ComplexType) -> T: ... @@ -261,14 +258,6 @@ class ConstraintType(Type): return visitor.visit_constraint_type(self) -@dataclass(frozen=True) -class UnionType(Type): - types: list[Type] - - def accept(self, visitor: Type.Visitor[T]) -> T: - return visitor.visit_union_type(self) - - @dataclass(frozen=True) class ComplexType(Type): properties: list[PropertyStmt] diff --git a/midas/ast/printer.py b/midas/ast/printer.py index ee59279..41dd6a0 100644 --- a/midas/ast/printer.py +++ b/midas/ast/printer.py @@ -252,17 +252,6 @@ class MidasAstPrinter( with self._child_level(single=True): type.constraint.accept(self) - def visit_union_type(self, type: m.UnionType) -> None: - self._write_line("UnionType") - with self._child_level(): - self._write_line("types", last=True) - with self._child_level(): - for i, type_ in enumerate(type.types): - self._idx = i - if i == len(type.types) - 1: - self._mark_last() - type_.accept(self) - def visit_complex_type(self, type: m.ComplexType) -> None: self._write_line("ComplexType") with self._child_level(): @@ -379,10 +368,6 @@ class MidasPrinter(m.Expr.Visitor[str], m.Stmt.Visitor[str], m.Type.Visitor[str] res += " where " + type.constraint.accept(self) return res - def visit_union_type(self, type: m.UnionType) -> str: - types: list[str] = [type_.accept(self) for type_ in type.types] - return " | ".join(types) - def visit_complex_type(self, type: m.ComplexType) -> str: res: str = "{\n" self.level += 1 diff --git a/midas/checker/types.py b/midas/checker/types.py index d9d91b7..d62c867 100644 --- a/midas/checker/types.py +++ b/midas/checker/types.py @@ -44,11 +44,4 @@ class ComplexType: properties: dict[str, Type] -@dataclass(frozen=True, kw_only=True) -class UnionType: - alternatives: list[Type] - - -Type = ( - BaseType | AliasType | UnknownType | UnitType | Function | ComplexType | UnionType -) +Type = BaseType | AliasType | UnknownType | UnitType | Function | ComplexType diff --git a/midas/cli/highlighter.py b/midas/cli/highlighter.py index 690c408..b1b705f 100644 --- a/midas/cli/highlighter.py +++ b/midas/cli/highlighter.py @@ -294,11 +294,6 @@ class MidasHighlighter( type.type.accept(self) type.constraint.accept(self) - def visit_union_type(self, type: m.UnionType) -> None: - self.wrap(type, "union-type") - for type_ in type.types: - type_.accept(self) - def visit_complex_type(self, type: m.ComplexType) -> None: self.wrap(type, "complex-type") for prop in type.properties: diff --git a/midas/cli/hl_midas.css b/midas/cli/hl_midas.css index f5b3d1d..fabb84e 100644 --- a/midas/cli/hl_midas.css +++ b/midas/cli/hl_midas.css @@ -8,7 +8,6 @@ span { &.named-type, &.generic-type, &.constraint-type, - &.union-type, &.complex-type { --col: 150, 150, 150; } diff --git a/midas/lexer/midas.py b/midas/lexer/midas.py index eec44d5..124ea09 100644 --- a/midas/lexer/midas.py +++ b/midas/lexer/midas.py @@ -18,8 +18,6 @@ class MidasLexer(Lexer): self.add_token(TokenType.LEFT_BRACE) case "}": self.add_token(TokenType.RIGHT_BRACE) - case "|": - self.add_token(TokenType.PIPE) case "<": self.add_token( TokenType.LESS_EQUAL if self.match("=") else TokenType.LESS diff --git a/midas/lexer/token.py b/midas/lexer/token.py index 9b30940..f08964a 100644 --- a/midas/lexer/token.py +++ b/midas/lexer/token.py @@ -23,7 +23,6 @@ class TokenType(Enum): AND = auto() QMARK = auto() DOT = auto() - PIPE = auto() # Operators # PLUS = auto() diff --git a/midas/parser/midas.py b/midas/parser/midas.py index 9af46da..5d09b83 100644 --- a/midas/parser/midas.py +++ b/midas/parser/midas.py @@ -20,7 +20,6 @@ from midas.ast.midas import ( Type, TypeStmt, UnaryExpr, - UnionType, VariableExpr, WildcardExpr, ) @@ -161,18 +160,7 @@ class MidasParser(Parser): Returns: TypeExpr: the parsed type expression """ - return self.union_type() - - def union_type(self) -> Type: - types: list[Type] = [self.constraint_type()] - while self.match(TokenType.PIPE): - types.append(self.constraint_type()) - if len(types) == 1: - return types[0] - return UnionType( - location=Location.span(types[0].location, types[-1].location), - types=types, - ) + return self.constraint_type() def constraint_type(self) -> Type: type: Type = self.base_type() diff --git a/midas/resolver/midas.py b/midas/resolver/midas.py index 9ffce85..acbbe96 100644 --- a/midas/resolver/midas.py +++ b/midas/resolver/midas.py @@ -4,7 +4,6 @@ import midas.ast.midas as m from midas.checker.types import ( AliasType, Type, - UnionType, UnknownType, ) from midas.resolver.builtin import define_builtins @@ -157,10 +156,6 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T # TODO return UnknownType() - def visit_union_type(self, type: m.UnionType) -> Type: - types: list[Type] = [type_.accept(self) for type_ in type.types] - return UnionType(alternatives=types) - def visit_complex_type(self, type: m.ComplexType) -> Type: for prop in type.properties: prop.accept(self) diff --git a/tests/serializer/midas.py b/tests/serializer/midas.py index 6da0dec..919dc66 100644 --- a/tests/serializer/midas.py +++ b/tests/serializer/midas.py @@ -19,7 +19,6 @@ from midas.ast.midas import ( Type, TypeStmt, UnaryExpr, - UnionType, VariableExpr, WildcardExpr, ) @@ -161,12 +160,6 @@ class MidasAstJsonSerializer( "constraint": type.constraint.accept(self), } - def visit_union_type(self, type: UnionType) -> dict: - return { - "_type": "UnionType", - "types": self._serialize_list(type.types), - } - def visit_complex_type(self, type: ComplexType) -> dict: return { "_type": "ComplexType", -- 2.49.1 From 9a934fabfd8be7e304d244e1ca1a3081461697ae Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 2 Jun 2026 17:22:19 +0200 Subject: [PATCH 16/22] tests: remove union type --- .../cases/midas-parser/01_simple_types.midas | 2 +- .../01_simple_types.midas.ref.json | 80 +++++++------------ 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/tests/cases/midas-parser/01_simple_types.midas b/tests/cases/midas-parser/01_simple_types.midas index a6c493d..6446790 100644 --- a/tests/cases/midas-parser/01_simple_types.midas +++ b/tests/cases/midas-parser/01_simple_types.midas @@ -48,7 +48,7 @@ type Person = { name: str // Property with an inline constraint - age: None | (int where (0 <= _ < 150)) + age: Optional[int where (0 <= _ < 150)] // Property referencing a predicate height: float where StrictlyPositive diff --git a/tests/cases/midas-parser/01_simple_types.midas.ref.json b/tests/cases/midas-parser/01_simple_types.midas.ref.json index 5d70b51..55b4813 100644 --- a/tests/cases/midas-parser/01_simple_types.midas.ref.json +++ b/tests/cases/midas-parser/01_simple_types.midas.ref.json @@ -1982,123 +1982,99 @@ }, { "type": "IDENTIFIER", - "lexeme": "None", + "lexeme": "Optional", "line": 51, "column": 10 }, { - "type": "WHITESPACE", - "lexeme": " ", + "type": "LEFT_BRACKET", + "lexeme": "[", "line": 51, - "column": 14 - }, - { - "type": "PIPE", - "lexeme": "|", - "line": 51, - "column": 15 - }, - { - "type": "WHITESPACE", - "lexeme": " ", - "line": 51, - "column": 16 - }, - { - "type": "LEFT_PAREN", - "lexeme": "(", - "line": 51, - "column": 17 + "column": 18 }, { "type": "IDENTIFIER", "lexeme": "int", "line": 51, - "column": 18 + "column": 19 }, { "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 21 + "column": 22 }, { "type": "WHERE", "lexeme": "where", "line": 51, - "column": 22 + "column": 23 }, { "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 27 + "column": 28 }, { "type": "LEFT_PAREN", "lexeme": "(", "line": 51, - "column": 28 + "column": 29 }, { "type": "NUMBER", "lexeme": "0", "line": 51, - "column": 29 + "column": 30 }, { "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 30 + "column": 31 }, { "type": "LESS_EQUAL", "lexeme": "<=", "line": 51, - "column": 31 + "column": 32 }, { "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 33 + "column": 34 }, { "type": "UNDERSCORE", "lexeme": "_", "line": 51, - "column": 34 + "column": 35 }, { "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 35 + "column": 36 }, { "type": "LESS", "lexeme": "<", "line": 51, - "column": 36 + "column": 37 }, { "type": "WHITESPACE", "lexeme": " ", "line": 51, - "column": 37 + "column": 38 }, { "type": "NUMBER", "lexeme": "150", "line": 51, - "column": 38 - }, - { - "type": "RIGHT_PAREN", - "lexeme": ")", - "line": 51, - "column": 41 + "column": 39 }, { "type": "RIGHT_PAREN", @@ -2106,11 +2082,17 @@ "line": 51, "column": 42 }, + { + "type": "RIGHT_BRACKET", + "lexeme": "]", + "line": 51, + "column": 43 + }, { "type": "NEWLINE", "lexeme": "\n", "line": 51, - "column": 43 + "column": 44 }, { "type": "NEWLINE", @@ -2651,12 +2633,12 @@ "_type": "PropertyStmt", "name": "age", "type": { - "_type": "UnionType", - "types": [ - { - "_type": "NamedType", - "name": "None" - }, + "_type": "GenericType", + "type": { + "_type": "NamedType", + "name": "Optional" + }, + "params": [ { "_type": "ConstraintType", "type": { -- 2.49.1 From 822a74acce563fd245e7b307cb3ca98eb9877d02 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Wed, 3 Jun 2026 13:03:41 +0200 Subject: [PATCH 17/22] refactor(checker): rename methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit improve a couple methods names, namely evaluate → type_of and evaluate_block → process_block --- midas/checker/checker.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index 82768ca..dda91a5 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -74,7 +74,7 @@ class Checker( message=message, ) - def evaluate(self, expr: p.Expr) -> Type: + def type_of(self, expr: p.Expr) -> Type: """Evaluate the type of an expression Args: @@ -85,7 +85,7 @@ class Checker( """ return expr.accept(self) - def evaluate_block(self, block: list[p.Stmt], env: Environment) -> bool: + def process_block(self, block: list[p.Stmt], env: Environment) -> bool: """Evaluate a sequence of statements Args: @@ -181,7 +181,7 @@ class Checker( self.logger.debug(f"Midas operations: {self.ctx._operations}") def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None: - self.evaluate(stmt.expr) + self.type_of(stmt.expr) def visit_function(self, stmt: p.Function) -> None: env: Environment = Environment(self.env) @@ -237,7 +237,7 @@ class Checker( ) self.env.define(stmt.name, inside_function) - returned: bool = self.evaluate_block(stmt.body, env) + returned: bool = self.process_block(stmt.body, env) inferred_return: Type = UnknownType() if not returned: env.return_types.append(UnitType()) @@ -278,7 +278,7 @@ class Checker( self.env.define(stmt.name, type) def visit_assign_stmt(self, stmt: p.AssignStmt) -> None: - value: Type = self.evaluate(stmt.value) + value: Type = self.type_of(stmt.value) for target in stmt.targets: if not isinstance(target, p.VariableExpr): self.logger.warning(f"Unsupported assignment to {target}") @@ -317,8 +317,8 @@ class Checker( ) env: Environment = Environment(self.env) - body_returned: bool = self.evaluate_block(stmt.body, env) - else_returned: bool = self.evaluate_block(stmt.orelse, env) + body_returned: bool = self.process_block(stmt.body, env) + else_returned: bool = self.process_block(stmt.orelse, env) self.env.return_types.extend(env.return_types) if body_returned and else_returned: raise ReturnException() @@ -329,8 +329,8 @@ class Checker( self.logger.warning(f"Unsupported operator {expr.operator}") self.warning(expr.location, f"Unsupported operator {expr.operator}") return UnknownType() - left: Type = self.evaluate(expr.left) - right: Type = self.evaluate(expr.right) + left: Type = self.type_of(expr.left) + right: Type = self.type_of(expr.right) result: Optional[Type] = self.ctx.get_operation_result(left, method, right) if result is None: @@ -347,8 +347,8 @@ class Checker( self.logger.warning(f"Unsupported operator {expr.operator}") self.warning(expr.location, f"Unsupported operator {expr.operator}") return UnknownType() - left: Type = self.evaluate(expr.left) - right: Type = self.evaluate(expr.right) + left: Type = self.type_of(expr.left) + right: Type = self.type_of(expr.right) result: Optional[Type] = self.ctx.get_operation_result(left, method, right) if result is None: @@ -365,7 +365,7 @@ class Checker( if path := self.parse_midas_import(expr): self.import_midas(path) return UnknownType() - callee: Type = self.evaluate(expr.callee) + callee: Type = self.type_of(expr.callee) if not isinstance(callee, Function): self.error(expr.callee.location, "Callee is not a function") return UnknownType() @@ -460,10 +460,10 @@ class Checker( list[MappedArgument]: the list of mapped arguments """ positional: list[tuple[p.Expr, Type]] = [ - (arg, self.evaluate(arg)) for arg in call.arguments + (arg, self.type_of(arg)) for arg in call.arguments ] keywords: dict[str, tuple[p.Expr, Type]] = { - name: (arg, self.evaluate(arg)) for name, arg in call.keywords.items() + name: (arg, self.type_of(arg)) for name, arg in call.keywords.items() } set_args: set[str] = set() -- 2.49.1 From 74b297c89cffd02074521a9e1c50d12bc4948542 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 5 Jun 2026 10:43:52 +0200 Subject: [PATCH 18/22] feat(checker): remove custom midas import remove custom import statement (`midas.using`) in favor of passing type definition files as arguments to the checker --- .../00_syntax_prototype/02_custom_types.py | 4 -- .../02_simple_types.py | 2 - midas/checker/checker.py | 48 ++++++------------- 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/examples/00_syntax_prototype/02_custom_types.py b/examples/00_syntax_prototype/02_custom_types.py index 16bf442..8678ba0 100644 --- a/examples/00_syntax_prototype/02_custom_types.py +++ b/examples/00_syntax_prototype/02_custom_types.py @@ -2,10 +2,6 @@ # ruff: disable[F821] from __future__ import annotations -# Prototype of custom type import to use valid Python syntax -import midas -midas.using("02_custom_types.midas") - # A data-frame using a custom type df: Frame[ location: GeoLocation diff --git a/examples/01_simple_type_checking/02_simple_types.py b/examples/01_simple_type_checking/02_simple_types.py index e964159..c015a75 100644 --- a/examples/01_simple_type_checking/02_simple_types.py +++ b/examples/01_simple_type_checking/02_simple_types.py @@ -1,8 +1,6 @@ # type: ignore # ruff: disable [F821] -midas.using("02_simple_types.midas") - distance: Meter = cast(Meter, 123.45) time: Second = cast(Second, 6.7) speed = distance / time diff --git a/midas/checker/checker.py b/midas/checker/checker.py index dda91a5..a96b472 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -34,9 +34,15 @@ class Checker( ): """A type checker which can use custom type definitions""" - def __init__(self, locals: dict[p.Expr, int], file_path: Path): + def __init__( + self, + locals: dict[p.Expr, int], + source_path: Path, + types_paths: list[Path], + ): self.logger: logging.Logger = logging.getLogger("Checker") - self.file_path: Path = file_path + self.source_path: Path = source_path + self.types_paths: list[Path] = types_paths self.ctx: MidasResolver = MidasResolver() self.global_env: Environment = Environment() self.env: Environment = self.global_env @@ -46,7 +52,7 @@ class Checker( def diagnostic(self, type: DiagnosticType, location: Location, message: str): self.diagnostics.append( Diagnostic( - file_path=self.file_path, + file_path=self.source_path, location=location, type=type, message=message, @@ -119,6 +125,12 @@ class Checker( list[Diagnostic]: the list of diagnostics (errors, warning, etc.) """ self.diagnostics = [] + + for path in self.types_paths: + self.import_midas(path) + self.logger.debug(f"Midas types: {self.ctx._types}") + self.logger.debug(f"Midas operations: {self.ctx._operations}") + for stmt in statements: stmt.accept(self) @@ -140,30 +152,6 @@ class Checker( return self.env.get_at(distance, name) return self.global_env.get(name) - def parse_midas_import(self, expr: p.CallExpr) -> Optional[Path]: - """Parse a Midas import statement - - The statement should be written as `midas.using("path/to/types.midas")` - - Args: - expr (p.CallExpr): the import call expression - - Returns: - Optional[Path]: the path to the imported file, or None if the expression is malformed - """ - match expr: - case p.CallExpr( - callee=p.GetExpr( - object=p.VariableExpr(name="midas"), - name="using", - ), - arguments=[ - p.LiteralExpr(value=path), - ], - ): - return Path(path) - return None - def import_midas(self, path: Path) -> None: """Import Midas definitions from a path @@ -171,14 +159,11 @@ class Checker( path (Path): the import path """ self.logger.debug(f"Importing type definitions from {path}") - path = (self.file_path.parent / path).resolve() lexer: MidasLexer = MidasLexer(path.read_text()) tokens: list[Token] = lexer.process() parser: MidasParser = MidasParser(tokens) stmts: list[m.Stmt] = parser.parse() self.ctx.resolve(stmts) - self.logger.debug(f"Midas types: {self.ctx._types}") - self.logger.debug(f"Midas operations: {self.ctx._operations}") def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None: self.type_of(stmt.expr) @@ -362,9 +347,6 @@ class Checker( def visit_unary_expr(self, expr: p.UnaryExpr) -> Type: ... def visit_call_expr(self, expr: p.CallExpr) -> Type: - if path := self.parse_midas_import(expr): - self.import_midas(path) - return UnknownType() callee: Type = self.type_of(expr.callee) if not isinstance(callee, Function): self.error(expr.callee.location, "Callee is not a function") -- 2.49.1 From 5d7c724bc8191b1bf7bbf9b689852871426fd340 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 5 Jun 2026 10:44:20 +0200 Subject: [PATCH 19/22] fix(cli): add types files argument --- midas/cli/main.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/midas/cli/main.py b/midas/cli/main.py index a9833bb..07dfd07 100644 --- a/midas/cli/main.py +++ b/midas/cli/main.py @@ -34,8 +34,9 @@ def midas(): @midas.command() @click.option("-l", "--highlight", type=click.File("w")) +@click.option("-t", "--types", type=click.File("r"), multiple=True) @click.argument("file", type=click.File("r")) -def compile(highlight: Optional[TextIO], file: TextIO): +def compile(highlight: Optional[TextIO], file: TextIO, types: tuple[TextIO]): logging.basicConfig(level=logging.DEBUG) source: str = file.read() tree: ast.Module = ast.parse(source, filename=file.name) @@ -43,7 +44,12 @@ def compile(highlight: Optional[TextIO], file: TextIO): stmts: list[p.Stmt] = parser.parse_module(tree) resolver = Resolver() resolver.resolve(*stmts) - checker = Checker(resolver.locals, file_path=Path(file.name).resolve()) + types_paths: list[Path] = [Path(t.name).resolve() for t in types] + checker = Checker( + resolver.locals, + source_path=Path(file.name).resolve(), + types_paths=types_paths, + ) diagnostics: list[Diagnostic] = checker.check(stmts) for diagnostic in diagnostics: print(diagnostic) -- 2.49.1 From 73b21789d5084d8558fc80e8935ae91f7e275db5 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 5 Jun 2026 10:48:46 +0200 Subject: [PATCH 20/22] fix(tests): remove custom imports --- tests/cases/checker/04_custom_types.py | 2 -- tests/cases/python-parser/02_custom_types.py | 4 ---- .../python-parser/02_custom_types.py.ref.json | 21 ------------------- tests/checker.py | 10 ++++++++- 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/tests/cases/checker/04_custom_types.py b/tests/cases/checker/04_custom_types.py index 93569b8..c015a75 100644 --- a/tests/cases/checker/04_custom_types.py +++ b/tests/cases/checker/04_custom_types.py @@ -1,8 +1,6 @@ # type: ignore # ruff: disable [F821] -midas.using("04_custom_types.midas") - distance: Meter = cast(Meter, 123.45) time: Second = cast(Second, 6.7) speed = distance / time diff --git a/tests/cases/python-parser/02_custom_types.py b/tests/cases/python-parser/02_custom_types.py index 32dbab3..1725bf4 100644 --- a/tests/cases/python-parser/02_custom_types.py +++ b/tests/cases/python-parser/02_custom_types.py @@ -2,10 +2,6 @@ # ruff: disable[F821] from __future__ import annotations -import midas - -midas.using("02_custom_types.midas") - df: Frame[ location: GeoLocation ] diff --git a/tests/cases/python-parser/02_custom_types.py.ref.json b/tests/cases/python-parser/02_custom_types.py.ref.json index 49b861f..639610d 100644 --- a/tests/cases/python-parser/02_custom_types.py.ref.json +++ b/tests/cases/python-parser/02_custom_types.py.ref.json @@ -1,26 +1,5 @@ { "stmts": [ - { - "_type": "ExpressionStmt", - "expr": { - "_type": "CallExpr", - "callee": { - "_type": "GetExpr", - "object": { - "_type": "VariableExpr", - "name": "midas" - }, - "name": "using" - }, - "arguments": [ - { - "_type": "LiteralExpr", - "value": "02_custom_types.midas" - } - ], - "keywords": {} - } - }, { "_type": "TypeAssign", "name": "df", diff --git a/tests/checker.py b/tests/checker.py index 0383132..d0a7b3e 100644 --- a/tests/checker.py +++ b/tests/checker.py @@ -33,6 +33,10 @@ class CheckerTester(Tester): if not path.is_file(): raise TypeError(f"Test '{path}' is not a file") + types_paths: list[Path] = [] + types_path: Path = path.with_suffix(".midas") + if types_path.exists(): + types_paths.append(types_path) source: str = path.read_text() tree: ast.Module = ast.parse(source, filename=path) parser = PythonParser() @@ -40,7 +44,11 @@ class CheckerTester(Tester): resolver = Resolver() resolver.resolve(*stmts) result: CaseResult = CaseResult() - checker = Checker(resolver.locals, file_path=path) + checker = Checker( + resolver.locals, + source_path=path, + types_paths=types_paths, + ) diagnostics: list[Diagnostic] = checker.check(stmts) for diagnostic in diagnostics: result.diagnostics.append( -- 2.49.1 From f182312cd274220fc10c56d4590d640f92b9fec9 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 5 Jun 2026 11:14:53 +0200 Subject: [PATCH 21/22] fix: update midas syntax definitions --- syntax/midas.ebnf | 20 ++++++++++----- syntax/midas.typ | 64 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/syntax/midas.ebnf b/syntax/midas.ebnf index 526e122..4626412 100644 --- a/syntax/midas.ebnf +++ b/syntax/midas.ebnf @@ -19,16 +19,24 @@ Comparison ::= Unary (ComparisonOp Unary)* Equality ::= Comparison (EqualityOp Comparison)* Constraint ::= Equality ("&" Equality)* -SimpleType ::= Identifier "?"? -Template ::= "[" Type "]" -Type ::= Identifier Template? "?"? +TemplateParam ::= Identifier ("<:" Type)? +Template ::= "[" (TemplateParam ("," TemplateParam)*)? "]" + + +TypeProperty ::= Identifier ":" Type +ComplexType ::= "{" TypeProperty* "}" +NamedType ::= Identifier +TypeParams ::= "[" (Type ("," Type)*)? "]" +GenericType ::= NamedType TypeParams? +GroupedType ::= "(" Type ")" +BaseType ::= GroupedType | ComplexType | GenericType +ConstraintType ::= BaseType ("where" Constraint)? +Type ::= ConstraintType -TypeProperty ::= Identifier ":" Type ("where" Constraints)? -ComplexTypeBody ::= "{" TypeProperty* "}" OpDefinition ::= "op" Identifier "(" Type ")" "->" Type ExtendBody ::= "{" OpDefinition* "}" -TypeStatement ::= "type" Identifier Template? ("(" Type ")" ("where" Constraint)? | ComplexTypeBody) +TypeStatement ::= "type" Identifier Template? "=" Type ExtendStatement ::= "extend" Type ExtendBody PredicateStatement ::= "predicate" Identifier "(" Identifier ":" Type ")" "=" Constraint diff --git a/syntax/midas.typ b/syntax/midas.typ index 3e16f19..71ad465 100644 --- a/syntax/midas.typ +++ b/syntax/midas.typ @@ -47,24 +47,52 @@ svg.railroad .terminal rect { {[`simple-type` 'identifier' ]} ``` -#let template = ``` -{[`template` "[" 'type' "]"]} +#let template-param = ``` +{[`template-param` 'identifier' ]} ``` -#let type = ``` -{[`type` 'identifier' ]} +#let template = ``` +{[`template` "[" "]"]} ``` #let type-property = ``` -{[`type-property` 'identifier' ":" 'type' ]} +{[`type-property` 'identifier' ":" 'type']} ``` -#let type-body = ``` -{[`type-body` "{" "}"]} +#let complex-type = ``` +{[`complex-type` "{" "}"]} +``` + +#let named-type = ``` +{[`named-type` 'identifier']} +``` + +#let type-params = ``` +{[`type-params` "[" "]"]} +``` + +#let generic-type = ``` +{[`generic-type` 'named-type' ]} +``` + +#let grouped-type = ``` +{[`grouped-type` "(" 'type' ")"]} +``` + +#let base-type = ``` +{[`base-type` <'grouped-type', 'complex-type', 'generic-type'>]} +``` + +#let constraint-type = ``` +{[`constraint-type` 'base-type' ]} +``` + +#let type = ``` +{[`type` 'constraint-type']} ``` #let type-statement = ``` -{[`type-statement` "type" 'identifier' <[["(" 'type' ")"] ], 'type-body'>]} +{[`type-statement` "type" 'identifier' "=" 'type']} ``` #let op-definition = ``` @@ -93,10 +121,17 @@ svg.railroad .terminal rect { equality: equality, constraint: constraint, simple-type: simple-type, + template-param: template-param, template: template, - type: type, type-property: type-property, - type-body: type-body, + complex-type: complex-type, + named-type: named-type, + type-params: type-params, + generic-type: generic-type, + grouped-type: grouped-type, + base-type: base-type, + constraint-type: constraint-type, + type: type, type-statement: type-statement, op-definition: op-definition, extend-statement: extend-statement, @@ -107,10 +142,17 @@ svg.railroad .terminal rect { #let inline = ( "grouping", "value", + "template-param", "template", "simple-type", "type-property", - "type-body", + "complex-type", + "type-params", + "named-type", + "grouped-type", + "generic-type", + "base-type", + "constraint-type", "op-definition", "type-statement", "extend-statement", -- 2.49.1 From ddcaebb51a32bc79e9466ed5db3867f22126f647 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 5 Jun 2026 11:19:29 +0200 Subject: [PATCH 22/22] fix: remove outdated syntax definition --- syntax/midas.typ | 6 ------ 1 file changed, 6 deletions(-) diff --git a/syntax/midas.typ b/syntax/midas.typ index 71ad465..b0b6438 100644 --- a/syntax/midas.typ +++ b/syntax/midas.typ @@ -43,10 +43,6 @@ svg.railroad .terminal rect { {[`constraint` 'equality'*"&"]} ``` -#let simple-type = ``` -{[`simple-type` 'identifier' ]} -``` - #let template-param = ``` {[`template-param` 'identifier' ]} ``` @@ -120,7 +116,6 @@ svg.railroad .terminal rect { comparison: comparison, equality: equality, constraint: constraint, - simple-type: simple-type, template-param: template-param, template: template, type-property: type-property, @@ -144,7 +139,6 @@ svg.railroad .terminal rect { "value", "template-param", "template", - "simple-type", "type-property", "complex-type", "type-params", -- 2.49.1