From 00fdda50341ddbb628fbec70787ad4b5d85fd2d1 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 10 Jul 2026 11:00:23 +0200 Subject: [PATCH 1/7] fix(checker): catch UndefinedMethodException where needed --- .../checker/frames/column_groupby_methods.py | 3 +++ midas/checker/frames/frame_groupby_methods.py | 2 ++ midas/checker/python.py | 23 ++++++++++++------- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/midas/checker/frames/column_groupby_methods.py b/midas/checker/frames/column_groupby_methods.py index 920a2b8..c92b08e 100644 --- a/midas/checker/frames/column_groupby_methods.py +++ b/midas/checker/frames/column_groupby_methods.py @@ -86,6 +86,9 @@ class ColumnGroupByMethodRegistry(MethodRegistry[Call]): # TODO: maybe better to filter arguments and pass some, in case the # return type depends on them + + # Don't catch UndefinedMethodException because all aggregation + # methods should be defined on columns too returns: Type = self.typer.call_method( location=call.location, call_expr=call.call_expr, diff --git a/midas/checker/frames/frame_groupby_methods.py b/midas/checker/frames/frame_groupby_methods.py index a2efba6..591c3ac 100644 --- a/midas/checker/frames/frame_groupby_methods.py +++ b/midas/checker/frames/frame_groupby_methods.py @@ -53,6 +53,8 @@ class FrameGroupByMethodRegistry(MethodRegistry[Call]): for column in call.groupby.frame.columns: with self.reporter.with_context(f"in column '{column.name}'"): column_groupby: ColumnGroupBy = ColumnGroupBy(column=column.type) + # Don't catch UndefinedMethodException because all aggregation + # methods should be defined on columns too result_type: Type = self.typer.call_method( location=call.location, call_expr=call.call_expr, diff --git a/midas/checker/python.py b/midas/checker/python.py index b2995d0..dd7eac8 100644 --- a/midas/checker/python.py +++ b/midas/checker/python.py @@ -772,14 +772,21 @@ class PythonTyper( match expr.callee: case p.GetExpr(object=obj, name=method): obj_type: Type = self.type_of(obj) - return self.call_method( - location=expr.location, - call_expr=expr, - obj=(obj, obj_type), - method_name=method, - positional=positional, - keywords=keywords, - ) + try: + return self.call_method( + location=expr.location, + call_expr=expr, + obj=(obj, obj_type), + method_name=method, + positional=positional, + keywords=keywords, + ) + except UndefinedMethodException: + self.reporter.error( + expr.location, + f"Unknown method {method} on type {obj_type}", + ) + return UnknownType() callee: Type = self.type_of(expr.callee) result: CallResult = self.dispatcher.get_result( From e61c4070da37e62bed770f246584c8489fb4c187 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 10 Jul 2026 11:04:49 +0200 Subject: [PATCH 2/7] fix(checker): handle returns outside functions --- midas/checker/python.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/midas/checker/python.py b/midas/checker/python.py index dd7eac8..755d11b 100644 --- a/midas/checker/python.py +++ b/midas/checker/python.py @@ -48,7 +48,9 @@ TypedExpr = tuple[p.Expr, Type] class ReturnException(Exception): - pass + def __init__(self, stmt: p.Stmt): + super().__init__() + self.stmt: p.Stmt = stmt class UndefinedMethodException(Exception): @@ -116,7 +118,10 @@ class PythonTyper( self.judgements = [] self.evaluated_casts = [] - self.check(stmts) + try: + self.check(stmts) + except ReturnException as e: + self.reporter.error(e.stmt.location, "Return statement outside of function") return TypedAST( stmts=stmts, @@ -576,7 +581,7 @@ class PythonTyper( def visit_return_stmt(self, stmt: p.ReturnStmt) -> None: type: Type = self.type_of(stmt.value) if stmt.value is not None else UnitType() self.env.return_types.append(type) - raise ReturnException() + raise ReturnException(stmt) def visit_if_stmt(self, stmt: p.IfStmt) -> None: # Not evaluated in sub-environment because assignments in the test leak out of the if @@ -599,7 +604,7 @@ class PythonTyper( 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() + raise ReturnException(stmt) def visit_pass(self, stmt: p.Pass) -> None: pass @@ -627,7 +632,7 @@ class PythonTyper( self.env = outer_env if body_returned: - raise ReturnException() + raise ReturnException(stmt) def visit_import_stmt(self, stmt: p.ImportStmt) -> None: self._visit_imports(stmt.location, stmt.imports) From 244e5f351a40b6adc21abf47b135aeec7f7c21f5 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 10 Jul 2026 11:13:32 +0200 Subject: [PATCH 3/7] fix(checker): handle subscript and error in typevar bound --- midas/checker/python.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/midas/checker/python.py b/midas/checker/python.py index 755d11b..361f99c 100644 --- a/midas/checker/python.py +++ b/midas/checker/python.py @@ -1090,10 +1090,13 @@ class PythonTyper( bound: Optional[Type] = None variance: Variance = Variance.INVARIANT if "bound" in call.keywords: - bound_type: p.MidasType = self._parse_type_from_expr( - call.keywords["bound"] - ) - bound = self.resolve_type_expr(bound_type) + try: + bound_type: p.MidasType = self._parse_type_from_expr( + call.keywords["bound"] + ) + bound = self.resolve_type_expr(bound_type) + except NotImplementedError: + bound = UnknownType() if is_kw_true("covariant"): variance = Variance.COVARIANT @@ -1139,6 +1142,14 @@ class PythonTyper( return parser._parse_type(node.body) case p.VariableExpr(name=name): return p.BaseType(location=location, base=name, args=()) + case p.SubscriptExpr(object=p.VariableExpr(name=name), index=arg): + args: tuple[p.MidasType, ...] = ( + tuple(self._parse_type_from_expr(a) for a in arg.items) + if isinstance(arg, p.TupleExpr) + else (self._parse_type_from_expr(arg),) + ) + return p.BaseType(location=location, base=name, args=args) + case _: raise NotImplementedError From 3ede752cae3035e6bc284a7d1ad3c3ea06bffe81 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 10 Jul 2026 11:19:06 +0200 Subject: [PATCH 4/7] fix(checker): handle already defined types --- midas/checker/midas.py | 10 ++++++++-- midas/checker/python.py | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/midas/checker/midas.py b/midas/checker/midas.py index 4ec4f73..452f9c1 100644 --- a/midas/checker/midas.py +++ b/midas/checker/midas.py @@ -170,7 +170,10 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type type = GenericType(name=name, params=params, body=type) else: type = DerivedType(name=name, type=type) - self.types.define_type(name, type) + try: + self.types.define_type(name, type) + except ValueError: + self.reporter.error(stmt.location, f"Type {name} already defined") self._local_variables.clear() self._current_name = None @@ -178,7 +181,10 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type name: str = stmt.name.lexeme self._current_name = name type: Type = stmt.type.accept(self) - self.types.define_type(name, type) + try: + self.types.define_type(name, type) + except ValueError: + self.reporter.error(stmt.location, f"Type {name} already defined") self._current_name = None def visit_member_stmt(self, stmt: m.MemberStmt) -> None: ... diff --git a/midas/checker/python.py b/midas/checker/python.py index 361f99c..2e1a08b 100644 --- a/midas/checker/python.py +++ b/midas/checker/python.py @@ -1111,7 +1111,13 @@ class PythonTyper( else: variance = Variance.CONTRAVARIANT var: TypeVar = TypeVar(name=name, bound=bound, variance=variance) - self.types.define_type(name, var) + try: + self.types.define_type(name, var) + except ValueError: + self.reporter.error( + call.location, + f"A type or type variable with the name {name} is already defined", + ) return var case _: From 4866ab209008b67b7949a2934cc6689141afe5ad Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 10 Jul 2026 11:20:39 +0200 Subject: [PATCH 5/7] fix(checker): handle already defined predicates --- midas/checker/midas.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/midas/checker/midas.py b/midas/checker/midas.py index 452f9c1..8d9b7ec 100644 --- a/midas/checker/midas.py +++ b/midas/checker/midas.py @@ -207,6 +207,7 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type ) def visit_predicate_stmt(self, stmt: m.PredicateStmt) -> None: + name: str = stmt.name.lexeme for spec in stmt.params: for param in spec.mixed: assert param.name is not None @@ -228,14 +229,17 @@ class MidasTyper(m.Stmt.Visitor[None], m.Expr.Visitor[Type], m.Type.Visitor[Type returns=type, ) self._predicate_params = {} - self.types.define_predicate( - stmt.name.lexeme, - Predicate( - type=type, - body=stmt.body, - alias=len(params) == 0, - ), - ) + try: + self.types.define_predicate( + name, + Predicate( + type=type, + body=stmt.body, + alias=len(params) == 0, + ), + ) + except ValueError: + self.reporter.error(stmt.location, f"Predicate {name} already defined") def _is_valid_predicate(self, body: Type) -> bool: """Check whether the given type is valid as a predicate's body From 90e9d167bf7d90e37175f7c8e1b2f6d9854919cf Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 10 Jul 2026 11:23:29 +0200 Subject: [PATCH 6/7] fix(checker): better handle invalid generic instantiation --- midas/checker/python.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/midas/checker/python.py b/midas/checker/python.py index 2e1a08b..5bd2151 100644 --- a/midas/checker/python.py +++ b/midas/checker/python.py @@ -1013,7 +1013,11 @@ class PythonTyper( if len(node.args) != 0: args: list[Type] = [self.resolve_type_expr(arg) for arg in node.args] - return self.types.apply_generic(base, args) + try: + return self.types.apply_generic(base, args) + except Exception as e: + self.reporter.error(node.location, f"Cannot apply generic type: {e}") + return UnknownType() return base def visit_frame_column(self, node: p.FrameColumn) -> ColumnType: From 720a82c2215fd102ee7e6fa981be4abf4f06a211 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 10 Jul 2026 11:30:10 +0200 Subject: [PATCH 7/7] fix(checker): remove redundant match case --- midas/checker/types.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/midas/checker/types.py b/midas/checker/types.py index 2becf9b..6aabc82 100644 --- a/midas/checker/types.py +++ b/midas/checker/types.py @@ -362,9 +362,6 @@ def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type: case UnknownType() | UnitType(): return type - case TopType() | GenericType(): - raise NotImplementedError(f"Unsupported type {type}") - # Ensure exhaustiveness case _: assert_never(type)