15 Commits
Author SHA1 Message Date
HEL 7c96f7a19f Merge pull request 'Undefined variables and column aggregations' (#43) from fix/undefined-variable into main
Tests / tests (push) Successful in 6s
Reviewed-on: #43
2026-07-23 23:36:12 +00:00
HEL e37432ad57 fix(checker): correct return type for column aggregation
Tests / tests (pull_request) Successful in 10s
2026-07-24 00:03:26 +02:00
HEL 3ab0dda682 fix(resolver): check undefined varaible in parent scopes 2026-07-19 15:51:46 +02:00
HEL 12c5309cff Merge pull request 'Avoid raising exceptions, report diagnostics instead' (#42) from fix/remove-raises into main
Tests / tests (push) Successful in 6s
Reviewed-on: #42
2026-07-10 09:35:02 +00:00
HEL 720a82c221 fix(checker): remove redundant match case
Tests / tests (pull_request) Successful in 6s
2026-07-10 11:30:10 +02:00
HEL 90e9d167bf fix(checker): better handle invalid generic instantiation 2026-07-10 11:23:29 +02:00
HEL 4866ab2090 fix(checker): handle already defined predicates 2026-07-10 11:20:39 +02:00
HEL 3ede752cae fix(checker): handle already defined types 2026-07-10 11:19:06 +02:00
HEL 244e5f351a fix(checker): handle subscript and error in typevar bound 2026-07-10 11:13:32 +02:00
HEL e61c4070da fix(checker): handle returns outside functions 2026-07-10 11:04:49 +02:00
HEL 00fdda5034 fix(checker): catch UndefinedMethodException where needed 2026-07-10 11:00:23 +02:00
HEL e302ed2377 Merge pull request 'Add Contributing and License sections to README' (#41) from feat/readme-additions into main
Tests / tests (push) Successful in 6s
Reviewed-on: #41
2026-07-09 22:39:09 +00:00
HEL 1d927e532f chore: add icon to README
Tests / tests (pull_request) Successful in 6s
2026-07-10 00:38:34 +02:00
HEL bfe4f854ea chore: add Contributing and License to README 2026-07-10 00:38:34 +02:00
HEL 3e06a236b5 Merge pull request 'Add Apache 2.0 license' (#40) from feat/license into main
Tests / tests (push) Successful in 6s
Reviewed-on: #40
2026-07-09 22:20:37 +00:00
8 changed files with 110 additions and 79 deletions
@@ -13,7 +13,6 @@ from midas.checker.types import (
Function,
ParamSpec,
Type,
UnknownType,
)
if TYPE_CHECKING:
@@ -86,6 +85,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,
@@ -94,11 +96,9 @@ class ColumnGroupByMethodRegistry(MethodRegistry[Call]):
positional=[],
keywords={},
)
if not isinstance(returns, ColumnType):
returns = ColumnType(type=UnknownType())
signature = Function(
params=ParamSpec(mixed=real_params),
returns=returns,
returns=ColumnType(type=returns),
)
result: CallResult = self.dispatcher.get_result(
+4 -6
View File
@@ -364,13 +364,11 @@ class ColumnMethodRegistry(MethodRegistry[Call]):
Type: the result type
"""
returns: Type = ColumnType(type=TopType())
returns: Type = TopType()
if formula:
returns = ColumnType(
type=self._resolve_formula_type(
call,
formula(call.column.type),
)
returns = self._resolve_formula_type(
call,
formula(call.column.type),
)
signature = Function(
params=ParamSpec(
@@ -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,
+20 -10
View File
@@ -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: ...
@@ -201,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
@@ -222,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
+52 -19
View File
@@ -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)
@@ -772,14 +777,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(
@@ -1001,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:
@@ -1078,10 +1094,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
@@ -1096,7 +1115,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 _:
@@ -1127,6 +1152,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
+16 -1
View File
@@ -97,6 +97,21 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
return True
return False
def is_defined(self, name: str) -> bool:
"""Check whether the given variable is defined
Args:
name (str): the name of the variable
Returns:
bool: `True` if the variable is defined in the closest scope where it
is declared, `False` otherwise
"""
for scope in reversed(self.scopes):
if name in scope:
return scope[name]
return False
def resolve_function(self, function: p.Function) -> None:
"""Resolve a function definition
@@ -230,7 +245,7 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]):
pass
def visit_variable_expr(self, expr: p.VariableExpr) -> None:
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name) is False:
if self.is_declared(expr.name) and not self.is_defined(expr.name):
self.reporter.error(
expr.location,
f"Variable '{expr.name}' is declared but may not be defined",
-3
View File
@@ -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)
+12 -36
View File
@@ -4305,9 +4305,7 @@
"arguments": [],
"keywords": {}
},
"type": {
"type": {}
}
"type": {}
},
{
"location": {
@@ -4342,9 +4340,7 @@
"arguments": [],
"keywords": {}
},
"type": {
"type": {}
}
"type": {}
},
{
"location": {
@@ -4380,9 +4376,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "int"
}
"name": "int"
}
},
{
@@ -4419,9 +4413,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "float"
}
"name": "float"
}
},
{
@@ -4458,9 +4450,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "int"
}
"name": "int"
}
},
{
@@ -4497,9 +4487,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "int"
}
"name": "int"
}
},
{
@@ -4536,9 +4524,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "int"
}
"name": "int"
}
},
{
@@ -4575,9 +4561,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "int"
}
"name": "int"
}
},
{
@@ -4614,9 +4598,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "int"
}
"name": "int"
}
},
{
@@ -4652,9 +4634,7 @@
"arguments": [],
"keywords": {}
},
"type": {
"type": {}
}
"type": {}
},
{
"location": {
@@ -4690,9 +4670,7 @@
"keywords": {}
},
"type": {
"type": {
"name": "int"
}
"name": "int"
}
},
{
@@ -4728,9 +4706,7 @@
"arguments": [],
"keywords": {}
},
"type": {
"type": {}
}
"type": {}
},
{
"location": {