From 82a0f132423d8b25aee1f48a8f260dc8d301887d Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 5 Jun 2026 14:17:24 +0200 Subject: [PATCH 01/15] feat(cli): add verbose flag to compile --- midas/cli/main.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/midas/cli/main.py b/midas/cli/main.py index f4adcef..95b6ca1 100644 --- a/midas/cli/main.py +++ b/midas/cli/main.py @@ -35,9 +35,15 @@ def midas(): @midas.command() @click.option("-l", "--highlight", type=click.File("w")) @click.option("-t", "--types", type=click.File("r"), multiple=True) +@click.option("-v", "--verbose", is_flag=True) @click.argument("file", type=click.File("r")) -def compile(highlight: Optional[TextIO], file: TextIO, types: tuple[TextIO]): - logging.basicConfig(level=logging.DEBUG) +def compile( + highlight: Optional[TextIO], + types: tuple[TextIO], + verbose: bool, + file: TextIO, +): + logging.basicConfig(level=logging.DEBUG if verbose else logging.WARN) source: str = file.read() tree: ast.Module = ast.parse(source, filename=file.name) parser = PythonParser() @@ -54,16 +60,17 @@ def compile(highlight: Optional[TextIO], file: TextIO, types: tuple[TextIO]): for diagnostic in diagnostics: print(diagnostic) - print( - json.dumps( - UniversalJSONDumper.dump( - checker.global_env, - [("Environment", "_children")], - lambda obj: isinstance(obj, get_args(Type)), - ), - indent=4, + if verbose: + print( + json.dumps( + UniversalJSONDumper.dump( + checker.global_env, + [("Environment", "_children")], + lambda obj: isinstance(obj, get_args(Type)), + ), + indent=4, + ) ) - ) if highlight is not None: highlighter = DiagnosticsHighlighter(source) highlighter.highlight(diagnostics) -- 2.49.1 From 1c3018812297a0c306e98dcbeb8c69b8d5cb3b13 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sat, 6 Jun 2026 16:25:33 +0200 Subject: [PATCH 02/15] feat(checker): record type judgements --- midas/checker/checker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index a96b472..8ca9b33 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -48,6 +48,7 @@ class Checker( self.env: Environment = self.global_env self.locals: dict[p.Expr, int] = locals self.diagnostics: list[Diagnostic] = [] + self.judgements: list[tuple[p.Expr, Type]] = [] def diagnostic(self, type: DiagnosticType, location: Location, message: str): self.diagnostics.append( @@ -89,7 +90,9 @@ class Checker( Returns: Type: the type of the given expression """ - return expr.accept(self) + type: Type = expr.accept(self) + self.judgements.append((expr, type)) + return type def process_block(self, block: list[p.Stmt], env: Environment) -> bool: """Evaluate a sequence of statements -- 2.49.1 From 67c40a3909da9fc6eec6807d4cd499dfad3f7309 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sat, 6 Jun 2026 16:30:04 +0200 Subject: [PATCH 03/15] feat(checker): add is_subtype method --- midas/checker/builtins.py | 4 ++ midas/checker/checker.py | 99 ++++++++++++++++++++++++++++++--------- 2 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 midas/checker/builtins.py diff --git a/midas/checker/builtins.py b/midas/checker/builtins.py new file mode 100644 index 0000000..bc80084 --- /dev/null +++ b/midas/checker/builtins.py @@ -0,0 +1,4 @@ +BUILTIN_SUBTYPES: dict[str, set[str]] = { + "float": {"int"}, + "int": {"bool"}, +} diff --git a/midas/checker/checker.py b/midas/checker/checker.py index 8ca9b33..aff3782 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -6,10 +6,19 @@ from typing import Optional import midas.ast.midas as m import midas.ast.python as p from midas.ast.location import Location +from midas.checker.builtins import BUILTIN_SUBTYPES 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 Function, Type, UnitType, UnknownType +from midas.checker.types import ( + AliasType, + BaseType, + ComplexType, + Function, + Type, + UnitType, + UnknownType, +) from midas.lexer.midas import MidasLexer from midas.lexer.token import Token from midas.parser.midas import MidasParser @@ -168,6 +177,45 @@ class Checker( stmts: list[m.Stmt] = parser.parse() self.ctx.resolve(stmts) + def unfold_type(self, type: Type) -> Type: + match type: + case AliasType(type=ref_type): + return self.unfold_type(ref_type) + case _: + return type + + def is_subtype(self, type1: Type, type2: Type) -> bool: + """Check whether `type1` is a subtype of `type2` + + For more details on the rules checked here, see TAPL Chap. 15-16-17 + + Args: + type1 (Type): the potential subtype + type2 (Type): the potential supertype + + Returns: + bool: whether `type1` is a subtype of `type2` + """ + + type1 = self.unfold_type(type1) + type2 = self.unfold_type(type2) + + if type1 == type2: + return True + + match (type1, type2): + case (BaseType(name=name1), BaseType(name=name2)): + return name1 in BUILTIN_SUBTYPES.get(name2, set()) + + case (ComplexType(properties=props1), ComplexType(properties=props2)): + for k, t in props2.items(): + if k not in props1: + return False + if self.is_subtype(props1[k], t): + return False + return True + + return False def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None: self.type_of(stmt.expr) @@ -266,7 +314,7 @@ class Checker( self.env.define(stmt.name, type) def visit_assign_stmt(self, stmt: p.AssignStmt) -> None: - value: Type = self.type_of(stmt.value) + value_type: 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}") @@ -276,13 +324,15 @@ class Checker( var_type: Optional[Type] = self.look_up_variable(name, target) if var_type is None: - self.env.define(name, value) + self.env.define(name, value_type) else: - # TODO: implement real comparison method - if var_type != value: + # S <: T + # Γ, x: T v: S + # x = v + if not self.is_subtype(value_type, var_type): self.error( stmt.location, - f"Cannot assign {value} to {name} of type {var_type}", + f"Cannot assign {value_type} to {name} of type {var_type}", ) def visit_return_stmt(self, stmt: p.ReturnStmt) -> None: @@ -357,7 +407,7 @@ class Checker( function: Function = callee mapped: list[MappedArgument] = self.map_call_arguments(function, expr) for arg in mapped: - if arg.type != arg.argument.type: + if not self.is_subtype(arg.type, arg.argument.type): self.error( arg.expr.location, f"Wrong type for argument '{arg.argument.name}', expected {arg.argument.type}, got {arg.type}", @@ -386,13 +436,17 @@ class Checker( 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 + + if self.is_subtype(left, right): + return right + if self.is_subtype(right, left): + return left + + self.error( + expr.location, + f"Incompatible operand types, {left=} and {right=}", + ) + return UnknownType() def visit_set_expr(self, expr: p.SetExpr) -> Type: ... @@ -410,13 +464,16 @@ 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}", - ) - return UnknownType() - return true_type + if self.is_subtype(true_type, false_type): + return false_type + if self.is_subtype(false_type, true_type): + return true_type + + self.error( + expr.location, + f"Incompatible types in ternary if branches: true={true_type} and false={false_type}", + ) + return UnknownType() def visit_base_type(self, node: p.BaseType) -> Type: return self.ctx.get_type(node.base) -- 2.49.1 From 2a2bb0aec753b70133b0d24a1deeb93b35a0e13c Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sat, 6 Jun 2026 16:50:42 +0200 Subject: [PATCH 04/15] feat(checker): store function param position --- midas/checker/checker.py | 7 +++++++ midas/checker/types.py | 1 + 2 files changed, 8 insertions(+) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index aff3782..dcf90b0 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -232,30 +232,37 @@ class Checker( return arg.default.accept(self) return UnknownType() + pos: int = 0 for arg in stmt.posonlyargs: pos_args.append( Function.Argument( + pos=pos, name=arg.name, type=eval_arg_type(arg), required=arg.default is None, ) ) + pos += 1 for arg in stmt.args: args.append( Function.Argument( + pos=pos, name=arg.name, type=eval_arg_type(arg), required=arg.default is None, ) ) + pos += 1 for arg in stmt.kwonlyargs: kw_args.append( Function.Argument( + pos=pos, # not relevant name=arg.name, type=eval_arg_type(arg), required=arg.default is None, ) ) + pos += 1 for arg in pos_args + args + kw_args: env.define(arg.name, arg.type) diff --git a/midas/checker/types.py b/midas/checker/types.py index d62c867..c1d4449 100644 --- a/midas/checker/types.py +++ b/midas/checker/types.py @@ -34,6 +34,7 @@ class Function: @dataclass(frozen=True, kw_only=True) class Argument: + pos: int name: str type: Type required: bool -- 2.49.1 From 5d440818479de02ea1ea26fb49c220159a3c7f02 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sat, 6 Jun 2026 16:53:52 +0200 Subject: [PATCH 05/15] feat(checker): implement function subtyping the logic for checking function subtypes is a WIP and has not been fully tested, there may be some errors and unhandled edge cases Claude helped lay out and verify the overall steps Co-authored-by: Claude --- midas/checker/checker.py | 113 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index dcf90b0..13b7855 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -215,7 +215,120 @@ class Checker( return False return True + case (Function(returns=return1), Function(returns=return2)): + if not self.is_func_subtype(type1, type2): + return False + if not self.is_subtype(return1, return2): + return False + return True + return False + + # TODO: verify the logic in here + def is_func_subtype(self, func1: Function, func2: Function) -> bool: + """Check whether a function is a subtype of another + + Args: + func1 (Function): the potential function subtype + func2 (Function): the potential function supertype + + Returns: + bool: whether `func1` is a subtype of `func2` + """ + if not self.is_subtype(func1.returns, func2.returns): + return False + + pos1: list[Function.Argument] = func1.pos_args + mixed1: list[Function.Argument] = func1.args + kw1: dict[str, Function.Argument] = {a.name: a for a in func1.kw_args} + pos2: list[Function.Argument] = func2.pos_args + mixed2: list[Function.Argument] = func2.args + kw2: dict[str, Function.Argument] = {a.name: a for a in func2.kw_args} + + mixed_by_pos: dict[int, Function.Argument] = {arg.pos: arg for arg in mixed2} + mixed_by_name: dict[str, Function.Argument] = {arg.name: arg for arg in mixed2} + + def is_arg_subtype(sub: Function.Argument, sup: Function.Argument) -> bool: + if not self.is_subtype(sub.type, sup.type): + return False + if not sup.required and sub.required: + return False + return True + + for arg1 in pos1: + arg2: Function.Argument + if arg1.pos < len(pos2): + arg2 = pos2[arg1.pos] + elif arg1.pos in mixed_by_pos: + arg2 = mixed_by_pos[arg1.pos] + elif not arg1.required: + continue + else: + return False + if not is_arg_subtype(arg2, arg1): + return False + + for name, arg1 in kw1.items(): + arg2: Function.Argument + if name in kw2: + arg2 = kw2[name] + elif name in mixed_by_name: + arg2 = mixed_by_name[name] + elif not arg1.required: + continue + else: + return False + if not is_arg_subtype(arg2, arg1): + return False + + for arg1 in mixed1: + pos_arg2: Optional[Function.Argument] = None + kw_arg2: Optional[Function.Argument] = None + if arg1.name in kw2: + kw_arg2 = kw2[arg1.name] + elif arg1.name in mixed_by_name: + kw_arg2 = mixed_by_name[arg1.name] + if arg1.pos < len(pos2): + pos_arg2 = pos2[arg1.pos] + elif arg1.pos in mixed_by_pos: + pos_arg2 = mixed_by_pos[arg1.pos] + + # No match in func2 and arg is required + if pos_arg2 is None and kw_arg2 is None and arg1.required: + return False + + # Matching keyword argument + if kw_arg2 is not None and not is_arg_subtype(kw_arg2, arg1): + return False + + # Matching positional argument + if pos_arg2 is not None and not is_arg_subtype(pos_arg2, arg1): + return False + + mixed_positions: set[int] = {a.pos for a in mixed1} + mixed_names: set[str] = {a.name for a in mixed1} + for arg2 in pos2: + if not arg2.required: + continue + if arg2.pos >= len(pos1) and arg2.pos not in mixed_positions: + return False + + for name, arg2 in kw2.items(): + if not arg2.required: + continue + if name not in kw1 and name not in mixed_names: + return False + + for arg2 in mixed2: + if arg2.required: + continue + pos_match: bool = arg2.pos < len(pos1) or arg2.pos in mixed_positions + kw_match: bool = arg2.name in kw1 or arg2.name in mixed_names + if not pos_match or not kw_match: + return False + + return True + def visit_expression_stmt(self, stmt: p.ExpressionStmt) -> None: self.type_of(stmt.expr) -- 2.49.1 From f0e3f7574fae2d6cf403757bb27df08637c5fd07 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sat, 6 Jun 2026 16:58:13 +0200 Subject: [PATCH 06/15] feat(tests): add judgements to test results add type judgements to checker test results and update all tests (including the new subtyping rules) --- tests/base.py | 6 +- .../cases/checker/01_simple_types.py.ref.json | 3 +- .../checker/02_simple_operations.py.ref.json | 165 ++- tests/cases/checker/03_functions.py.ref.json | 1226 ++++++++++++++++- .../cases/checker/04_custom_types.py.ref.json | 108 +- .../cases/checker/05_control_flow.py.ref.json | 210 +++ tests/checker.py | 19 + 7 files changed, 1708 insertions(+), 29 deletions(-) diff --git a/tests/base.py b/tests/base.py index 630bfa1..0749d79 100644 --- a/tests/base.py +++ b/tests/base.py @@ -29,7 +29,7 @@ class Tester(ABC): def _list_tests(self) -> list[Path]: ... def run_all_tests(self) -> bool: - paths: list[Path] = self._list_tests() + paths: list[Path] = sorted(self._list_tests()) return self.run_tests(paths) def run_tests(self, tests: list[Path]) -> bool: @@ -40,7 +40,7 @@ class Tester(ABC): print(rule) for i, test in enumerate(tests): - print(f"Case {i+1}/{n}: {test.relative_to(self.CASES_DIR)}") + print(f"Case {i+1}/{n}: {test.resolve().relative_to(self.CASES_DIR)}") success: bool = self._run_test(test) if success: successes += 1 @@ -78,7 +78,7 @@ class Tester(ABC): def _exec_case(self, path: Path) -> CaseResult: ... def update_all_tests(self): - paths: list[Path] = self._list_tests() + paths: list[Path] = sorted(self._list_tests()) return self.update_tests(paths) def update_tests(self, tests: list[Path]): diff --git a/tests/cases/checker/01_simple_types.py.ref.json b/tests/cases/checker/01_simple_types.py.ref.json index c37fb01..3c4d0b9 100644 --- a/tests/cases/checker/01_simple_types.py.ref.json +++ b/tests/cases/checker/01_simple_types.py.ref.json @@ -1,3 +1,4 @@ { - "diagnostics": [] + "diagnostics": [], + "judgments": [] } \ No newline at end of file diff --git a/tests/cases/checker/02_simple_operations.py.ref.json b/tests/cases/checker/02_simple_operations.py.ref.json index c390e27..6952790 100644 --- a/tests/cases/checker/02_simple_operations.py.ref.json +++ b/tests/cases/checker/02_simple_operations.py.ref.json @@ -27,20 +27,165 @@ ] }, "message": "Undefined operation __add__ between BaseType(name='bool') and BaseType(name='bool')" + } + ], + "judgments": [ + { + "location": { + "from": "L1:9", + "to": "L1:10" + }, + "expr": { + "_type": "LiteralExpr", + "value": 3 + }, + "type": { + "name": "int" + } }, { - "type": "Error", "location": { - "start": [ - 11, - 0 - ], - "end": [ - 11, - 12 - ] + "from": "L2:9", + "to": "L2:10" }, - "message": "Cannot assign BaseType(name='int') to f of type BaseType(name='float')" + "expr": { + "_type": "LiteralExpr", + "value": 4 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L4:4", + "to": "L4:5" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L4:8", + "to": "L4:9" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L4:4", + "to": "L4:9" + }, + "expr": { + "_type": "BinaryExpr", + "left": { + "_type": "VariableExpr", + "name": "a" + }, + "operator": "+", + "right": { + "_type": "VariableExpr", + "name": "b" + } + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L6:4", + "to": "L6:13" + }, + "expr": { + "_type": "LiteralExpr", + "value": "invalid" + }, + "type": { + "name": "str" + } + }, + { + "location": { + "from": "L8:4", + "to": "L8:8" + }, + "expr": { + "_type": "LiteralExpr", + "value": true + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L9:4", + "to": "L9:5" + }, + "expr": { + "_type": "VariableExpr", + "name": "d" + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L9:8", + "to": "L9:9" + }, + "expr": { + "_type": "VariableExpr", + "name": "d" + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L9:4", + "to": "L9:9" + }, + "expr": { + "_type": "BinaryExpr", + "left": { + "_type": "VariableExpr", + "name": "d" + }, + "operator": "+", + "right": { + "_type": "VariableExpr", + "name": "d" + } + }, + "type": {} + }, + { + "location": { + "from": "L11:11", + "to": "L11:12" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } } ] } \ No newline at end of file diff --git a/tests/cases/checker/03_functions.py.ref.json b/tests/cases/checker/03_functions.py.ref.json index 40b33b5..cd0ce42 100644 --- a/tests/cases/checker/03_functions.py.ref.json +++ b/tests/cases/checker/03_functions.py.ref.json @@ -238,20 +238,6 @@ }, "message": "Wrong type for argument 'a', expected BaseType(name='int'), got BaseType(name='str')" }, - { - "type": "Error", - "location": { - "start": [ - 18, - 15 - ], - "end": [ - 18, - 16 - ] - }, - "message": "Wrong type for argument 'b', expected BaseType(name='float'), got BaseType(name='int')" - }, { "type": "Error", "location": { @@ -266,5 +252,1217 @@ }, "message": "Wrong type for argument 'c', expected BaseType(name='str'), got BaseType(name='bool')" } + ], + "judgments": [ + { + "location": { + "from": "L5:5", + "to": "L5:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L5:5", + "to": "L5:10" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [], + "keywords": {} + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L6:5", + "to": "L6:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L6:9", + "to": "L6:10" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L6:5", + "to": "L6:11" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + } + ], + "keywords": {} + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L7:5", + "to": "L7:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L7:9", + "to": "L7:10" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L7:12", + "to": "L7:15" + }, + "expr": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L7:5", + "to": "L7:16" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + }, + { + "_type": "LiteralExpr", + "value": 2.0 + } + ], + "keywords": {} + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L8:5", + "to": "L8:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L8:9", + "to": "L8:10" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L8:14", + "to": "L8:17" + }, + "expr": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L8:5", + "to": "L8:18" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + } + ], + "keywords": { + "b": { + "_type": "LiteralExpr", + "value": 2.0 + } + } + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L9:5", + "to": "L9:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L9:9", + "to": "L9:10" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L9:12", + "to": "L9:15" + }, + "expr": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L9:17", + "to": "L9:23" + }, + "expr": { + "_type": "LiteralExpr", + "value": "test" + }, + "type": { + "name": "str" + } + }, + { + "location": { + "from": "L9:5", + "to": "L9:24" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + }, + { + "_type": "LiteralExpr", + "value": 2.0 + }, + { + "_type": "LiteralExpr", + "value": "test" + } + ], + "keywords": {} + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L10:5", + "to": "L10:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L10:9", + "to": "L10:10" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L10:12", + "to": "L10:15" + }, + "expr": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L10:19", + "to": "L10:22" + }, + "expr": { + "_type": "LiteralExpr", + "value": 3.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L10:5", + "to": "L10:23" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + }, + { + "_type": "LiteralExpr", + "value": 2.0 + } + ], + "keywords": { + "b": { + "_type": "LiteralExpr", + "value": 3.0 + } + } + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L11:5", + "to": "L11:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L11:11", + "to": "L11:12" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L11:5", + "to": "L11:13" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [], + "keywords": { + "a": { + "_type": "LiteralExpr", + "value": 1 + } + } + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L12:5", + "to": "L12:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L12:11", + "to": "L12:17" + }, + "expr": { + "_type": "LiteralExpr", + "value": "test" + }, + "type": { + "name": "str" + } + }, + { + "location": { + "from": "L12:5", + "to": "L12:18" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [], + "keywords": { + "g": { + "_type": "LiteralExpr", + "value": "test" + } + } + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L14:6", + "to": "L14:9" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L14:10", + "to": "L14:11" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L14:13", + "to": "L14:16" + }, + "expr": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L14:20", + "to": "L14:26" + }, + "expr": { + "_type": "LiteralExpr", + "value": "test" + }, + "type": { + "name": "str" + } + }, + { + "location": { + "from": "L14:6", + "to": "L14:27" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + }, + { + "_type": "LiteralExpr", + "value": 2.0 + } + ], + "keywords": { + "c": { + "_type": "LiteralExpr", + "value": "test" + } + } + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L15:6", + "to": "L15:9" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L15:10", + "to": "L15:11" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L15:15", + "to": "L15:18" + }, + "expr": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L15:22", + "to": "L15:28" + }, + "expr": { + "_type": "LiteralExpr", + "value": "test" + }, + "type": { + "name": "str" + } + }, + { + "location": { + "from": "L15:6", + "to": "L15:29" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + } + ], + "keywords": { + "b": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "c": { + "_type": "LiteralExpr", + "value": "test" + } + } + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L16:6", + "to": "L16:9" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L16:10", + "to": "L16:11" + }, + "expr": { + "_type": "LiteralExpr", + "value": 1 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L16:15", + "to": "L16:21" + }, + "expr": { + "_type": "LiteralExpr", + "value": "test" + }, + "type": { + "name": "str" + } + }, + { + "location": { + "from": "L16:25", + "to": "L16:28" + }, + "expr": { + "_type": "LiteralExpr", + "value": 2.0 + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L16:6", + "to": "L16:29" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": 1 + } + ], + "keywords": { + "c": { + "_type": "LiteralExpr", + "value": "test" + }, + "b": { + "_type": "LiteralExpr", + "value": 2.0 + } + } + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L18:6", + "to": "L18:9" + }, + "expr": { + "_type": "VariableExpr", + "name": "foo" + }, + "type": { + "name": "foo", + "pos_args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "int" + }, + "required": true + } + ], + "args": [ + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [ + { + "pos": 2, + "name": "c", + "type": { + "name": "str" + }, + "required": true + } + ], + "returns": { + "name": "bool" + } + } + }, + { + "location": { + "from": "L18:10", + "to": "L18:13" + }, + "expr": { + "_type": "LiteralExpr", + "value": "a" + }, + "type": { + "name": "str" + } + }, + { + "location": { + "from": "L18:15", + "to": "L18:16" + }, + "expr": { + "_type": "LiteralExpr", + "value": 3 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L18:20", + "to": "L18:25" + }, + "expr": { + "_type": "LiteralExpr", + "value": false + }, + "type": { + "name": "bool" + } + }, + { + "location": { + "from": "L18:6", + "to": "L18:26" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "foo" + }, + "arguments": [ + { + "_type": "LiteralExpr", + "value": "a" + }, + { + "_type": "LiteralExpr", + "value": 3 + } + ], + "keywords": { + "c": { + "_type": "LiteralExpr", + "value": false + } + } + }, + "type": { + "name": "bool" + } + } ] } \ No newline at end of file diff --git a/tests/cases/checker/04_custom_types.py.ref.json b/tests/cases/checker/04_custom_types.py.ref.json index c37fb01..1802082 100644 --- a/tests/cases/checker/04_custom_types.py.ref.json +++ b/tests/cases/checker/04_custom_types.py.ref.json @@ -1,3 +1,109 @@ { - "diagnostics": [] + "diagnostics": [], + "judgments": [ + { + "location": { + "from": "L4:18", + "to": "L4:37" + }, + "expr": { + "_type": "CastExpr", + "type": { + "_type": "BaseType", + "base": "Meter", + "param": null + }, + "expr": { + "_type": "LiteralExpr", + "value": 123.45 + } + }, + "type": { + "name": "Meter", + "type": { + "name": "float" + } + } + }, + { + "location": { + "from": "L5:15", + "to": "L5:32" + }, + "expr": { + "_type": "CastExpr", + "type": { + "_type": "BaseType", + "base": "Second", + "param": null + }, + "expr": { + "_type": "LiteralExpr", + "value": 6.7 + } + }, + "type": { + "name": "Second", + "type": { + "name": "float" + } + } + }, + { + "location": { + "from": "L6:8", + "to": "L6:16" + }, + "expr": { + "_type": "VariableExpr", + "name": "distance" + }, + "type": { + "name": "Meter", + "type": { + "name": "float" + } + } + }, + { + "location": { + "from": "L6:19", + "to": "L6:23" + }, + "expr": { + "_type": "VariableExpr", + "name": "time" + }, + "type": { + "name": "Second", + "type": { + "name": "float" + } + } + }, + { + "location": { + "from": "L6:8", + "to": "L6:23" + }, + "expr": { + "_type": "BinaryExpr", + "left": { + "_type": "VariableExpr", + "name": "distance" + }, + "operator": "/", + "right": { + "_type": "VariableExpr", + "name": "time" + } + }, + "type": { + "name": "MeterPerSecond", + "type": { + "name": "float" + } + } + } + ] } \ No newline at end of file diff --git a/tests/cases/checker/05_control_flow.py.ref.json b/tests/cases/checker/05_control_flow.py.ref.json index a68a7b9..8f031f2 100644 --- a/tests/cases/checker/05_control_flow.py.ref.json +++ b/tests/cases/checker/05_control_flow.py.ref.json @@ -42,5 +42,215 @@ }, "message": "Mixed return types: [BaseType(name='int'), BaseType(name='str')]" } + ], + "judgments": [ + { + "location": { + "from": "L2:11", + "to": "L2:12" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L2:15", + "to": "L2:16" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L5:7", + "to": "L5:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L5:11", + "to": "L5:12" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L6:15", + "to": "L6:16" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L6:19", + "to": "L6:20" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L8:15", + "to": "L8:16" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L8:19", + "to": "L8:20" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L15:7", + "to": "L15:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L15:11", + "to": "L15:13" + }, + "expr": { + "_type": "LiteralExpr", + "value": 10 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L16:15", + "to": "L16:16" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L16:19", + "to": "L16:21" + }, + "expr": { + "_type": "LiteralExpr", + "value": 10 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L22:7", + "to": "L22:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L22:11", + "to": "L22:12" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L23:15", + "to": "L23:16" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L23:19", + "to": "L23:20" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "int" + } + } ] } \ No newline at end of file diff --git a/tests/checker.py b/tests/checker.py index d0a7b3e..27a94cb 100644 --- a/tests/checker.py +++ b/tests/checker.py @@ -6,14 +6,17 @@ from pathlib import Path import midas.ast.python as p from midas.checker.checker import Checker from midas.checker.diagnostic import Diagnostic +from midas.checker.types import Type from midas.parser.python import PythonParser from midas.resolver.resolver import Resolver from tests.base import Tester +from tests.serializer.python import PythonAstJsonSerializer @dataclass class CaseResult: diagnostics: list[dict] = field(default_factory=list) + judgments: list = field(default_factory=list) def dumps(self) -> str: return json.dumps(asdict(self), indent=2) @@ -49,6 +52,7 @@ class CheckerTester(Tester): source_path=path, types_paths=types_paths, ) + diagnostics: list[Diagnostic] = checker.check(stmts) for diagnostic in diagnostics: result.diagnostics.append( @@ -68,6 +72,21 @@ class CheckerTester(Tester): } ) + judgements: list[tuple[p.Expr, Type]] = checker.judgements + serializer = PythonAstJsonSerializer() + for expr, type in judgements: + loc = expr.location + result.judgments.append( + { + "location": { + "from": f"L{loc.lineno}:{loc.col_offset}", + "to": f"L{loc.end_lineno}:{loc.end_col_offset}", + }, + "expr": expr.accept(serializer), + "type": asdict(type), + } + ) + return result -- 2.49.1 From bccd75317e10cae76364ca37737a0112dfc20a42 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sat, 6 Jun 2026 16:59:49 +0200 Subject: [PATCH 07/15] tests: add subtyping test --- tests/cases/checker/06_subtyping.py | 12 + tests/cases/checker/06_subtyping.py.ref.json | 220 +++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 tests/cases/checker/06_subtyping.py create mode 100644 tests/cases/checker/06_subtyping.py.ref.json diff --git a/tests/cases/checker/06_subtyping.py b/tests/cases/checker/06_subtyping.py new file mode 100644 index 0000000..c334ab8 --- /dev/null +++ b/tests/cases/checker/06_subtyping.py @@ -0,0 +1,12 @@ +v1: int = 3 +v2: float = 4 + + +def maximum(a: float, b: float): + if b > a: + return b + return a + + +v3 = maximum(v1, v2) +v3 = v1 + v2 diff --git a/tests/cases/checker/06_subtyping.py.ref.json b/tests/cases/checker/06_subtyping.py.ref.json new file mode 100644 index 0000000..c3482bc --- /dev/null +++ b/tests/cases/checker/06_subtyping.py.ref.json @@ -0,0 +1,220 @@ +{ + "diagnostics": [ + { + "type": "Error", + "location": { + "start": [ + 12, + 5 + ], + "end": [ + 12, + 12 + ] + }, + "message": "Undefined operation __add__ between BaseType(name='int') and BaseType(name='float')" + }, + { + "type": "Error", + "location": { + "start": [ + 12, + 0 + ], + "end": [ + 12, + 12 + ] + }, + "message": "Cannot assign UnknownType() to v3 of type BaseType(name='float')" + } + ], + "judgments": [ + { + "location": { + "from": "L1:10", + "to": "L1:11" + }, + "expr": { + "_type": "LiteralExpr", + "value": 3 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L2:12", + "to": "L2:13" + }, + "expr": { + "_type": "LiteralExpr", + "value": 4 + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L6:7", + "to": "L6:8" + }, + "expr": { + "_type": "VariableExpr", + "name": "b" + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L6:11", + "to": "L6:12" + }, + "expr": { + "_type": "VariableExpr", + "name": "a" + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L11:5", + "to": "L11:12" + }, + "expr": { + "_type": "VariableExpr", + "name": "maximum" + }, + "type": { + "name": "maximum", + "pos_args": [], + "args": [ + { + "pos": 0, + "name": "a", + "type": { + "name": "float" + }, + "required": true + }, + { + "pos": 1, + "name": "b", + "type": { + "name": "float" + }, + "required": true + } + ], + "kw_args": [], + "returns": { + "name": "float" + } + } + }, + { + "location": { + "from": "L11:13", + "to": "L11:15" + }, + "expr": { + "_type": "VariableExpr", + "name": "v1" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L11:17", + "to": "L11:19" + }, + "expr": { + "_type": "VariableExpr", + "name": "v2" + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L11:5", + "to": "L11:20" + }, + "expr": { + "_type": "CallExpr", + "callee": { + "_type": "VariableExpr", + "name": "maximum" + }, + "arguments": [ + { + "_type": "VariableExpr", + "name": "v1" + }, + { + "_type": "VariableExpr", + "name": "v2" + } + ], + "keywords": {} + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L12:5", + "to": "L12:7" + }, + "expr": { + "_type": "VariableExpr", + "name": "v1" + }, + "type": { + "name": "int" + } + }, + { + "location": { + "from": "L12:10", + "to": "L12:12" + }, + "expr": { + "_type": "VariableExpr", + "name": "v2" + }, + "type": { + "name": "float" + } + }, + { + "location": { + "from": "L12:5", + "to": "L12:12" + }, + "expr": { + "_type": "BinaryExpr", + "left": { + "_type": "VariableExpr", + "name": "v1" + }, + "operator": "+", + "right": { + "_type": "VariableExpr", + "name": "v2" + } + }, + "type": {} + } + ] +} \ No newline at end of file -- 2.49.1 From 25bd895dde07cfa99c5cf93ac58438ea1739cc59 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 13:42:15 +0200 Subject: [PATCH 08/15] feat(cli): improve diagnostic printing --- midas/checker/diagnostic.py | 8 +++-- midas/cli/ansi.py | 41 ++++++++++++++++++++++++++ midas/cli/main.py | 58 +++++++++++++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 midas/cli/ansi.py diff --git a/midas/checker/diagnostic.py b/midas/checker/diagnostic.py index 45514b4..77f687e 100644 --- a/midas/checker/diagnostic.py +++ b/midas/checker/diagnostic.py @@ -19,7 +19,8 @@ class Diagnostic: type: DiagnosticType message: str - def __str__(self) -> str: + @property + def location_str(self) -> str: start_loc: str = f"L{self.location.lineno}:{self.location.col_offset+1}" end_loc: Optional[str] = "" if ( @@ -30,4 +31,7 @@ class Diagnostic: loc: str = ( f"at {start_loc}" if end_loc is None else f"from {start_loc} to {end_loc}" ) - return f"{self.type} in {self.file_path} {loc}: {self.message}" + return f"{self.type} in {self.file_path} {loc}" + + def __str__(self) -> str: + return f"{self.location_str}: {self.message}" diff --git a/midas/cli/ansi.py b/midas/cli/ansi.py new file mode 100644 index 0000000..f929be4 --- /dev/null +++ b/midas/cli/ansi.py @@ -0,0 +1,41 @@ +class Ansi: + CTRL = "\x1b[" + RESET = CTRL + "0m" + BOLD = CTRL + "1m" + DIM = CTRL + "2m" + ITALIC = CTRL + "3m" + UNDERLINE = CTRL + "4m" + + BLACK = 0 + RED = 1 + GREEN = 2 + YELLOW = 3 + BLUE = 4 + MAGENTA = 5 + CYAN = 6 + WHITE = 7 + + BRIGHT_BLACK = 60 + BRIGHT_RED = 61 + BRIGHT_GREEN = 62 + BRIGHT_YELLOW = 63 + BRIGHT_BLUE = 64 + BRIGHT_MAGENTA = 65 + BRIGHT_CYAN = 66 + BRIGHT_WHITE = 67 + + @classmethod + def FG(cls, col: int) -> str: + return f"{cls.CTRL}{30 + col}m" + + @classmethod + def BG(cls, col: int) -> str: + return f"{cls.CTRL}{40 + col}m" + + @classmethod + def FG_RGB(cls, r: int, g: int, b: int) -> str: + return f"{cls.CTRL}38;2;{r};{g};{b}m" + + @classmethod + def BG_RGB(cls, r: int, g: int, b: int) -> str: + return f"{cls.CTRL}48;2;{r};{g};{b}m" diff --git a/midas/cli/main.py b/midas/cli/main.py index 95b6ca1..ae4295b 100644 --- a/midas/cli/main.py +++ b/midas/cli/main.py @@ -8,10 +8,12 @@ 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, MidasPrinter, PythonAstPrinter from midas.checker.checker import Checker -from midas.checker.diagnostic import Diagnostic +from midas.checker.diagnostic import Diagnostic, DiagnosticType from midas.checker.types import Type +from midas.cli.ansi import Ansi from midas.cli.highlighter import ( DiagnosticsHighlighter, Highlighter, @@ -32,6 +34,57 @@ def midas(): pass +def print_diagnostic(lines: list[str], diagnostic: Diagnostic, indent: int = 4): + """Pretty-print a diagnostic, showing some context if possible + + If the diagnostic concerns a specific part of one line, the line is shown + with the affected part highlighted. The message is clearly printed under the + line with an underline further indicating the target expression. + + If multiple lines are concerned, no context is shown, only the + diagnostic type, location and message + + Args: + lines (list[str]): source code lines + diagnostic (Diagnostic): the diagnostic to print + indent (int, optional): the number of spaces added before the target line to indent if from the location header. Defaults to 4. + """ + + loc: Location = diagnostic.location + if loc.lineno != loc.end_lineno: + print(diagnostic) + return + + start_offset: int = loc.col_offset + end_offset: int = loc.end_col_offset or (start_offset + 1) + + line: str = lines[loc.lineno - 1] + before: str = line[:start_offset] + after: str = line[end_offset:] + + color: int = { + DiagnosticType.ERROR: Ansi.RED, + DiagnosticType.WARNING: Ansi.YELLOW, + DiagnosticType.INFO: Ansi.CYAN, + }.get(diagnostic.type, Ansi.WHITE) + + subject: str = Ansi.FG(color) + line[start_offset:end_offset] + Ansi.RESET + cursor: str = ( + " " * start_offset + + Ansi.FG(color) + + "~" * (end_offset - start_offset) + + "> " + + diagnostic.message + + Ansi.RESET + ) + + indent_str: str = " " * indent + print(diagnostic.location_str + ":") + print(indent_str + before + subject + after) + print(indent_str + cursor) + print() + + @midas.command() @click.option("-l", "--highlight", type=click.File("w")) @click.option("-t", "--types", type=click.File("r"), multiple=True) @@ -57,8 +110,9 @@ def compile( types_paths=types_paths, ) diagnostics: list[Diagnostic] = checker.check(stmts) + lines: list[str] = source.split("\n") for diagnostic in diagnostics: - print(diagnostic) + print_diagnostic(lines, diagnostic) if verbose: print( -- 2.49.1 From c24eb5125ef2c02d392fa97148c8bb48559ffdcd Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 13:43:43 +0200 Subject: [PATCH 09/15] feat(checker): resolve operation overloads with subtypes --- midas/checker/checker.py | 41 +++++++++++++++++++++++++++++++++++++--- midas/checker/types.py | 12 ++++++++++++ midas/resolver/midas.py | 33 ++++++++++++++++++++++++++------ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index 13b7855..9ed50f0 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -15,6 +15,7 @@ from midas.checker.types import ( BaseType, ComplexType, Function, + Operation, Type, UnitType, UnknownType, @@ -490,14 +491,48 @@ class Checker( 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: + operations: list[Operation] = self.ctx.get_operations_by_name(method) + valid_operations: list[Operation] = [] + for op in operations: + sig: Operation.CallSignature = op.signature + if self.is_subtype(left, sig.left) and self.is_subtype(right, sig.right): + valid_operations.append(op) + + if len(valid_operations) == 0: self.error( expr.location, f"Undefined operation {method} between {left} and {right}", ) return UnknownType() - return result + elif len(valid_operations) == 1: + self.logger.debug(f"Unique operation {method} between {left} and {right}") + return valid_operations[0].result + + for i, op1 in enumerate(valid_operations): + sig1: Operation.CallSignature = op1.signature + best_match: bool = True + for j, op2 in enumerate(valid_operations): + if i == j: + continue + sig2: Operation.CallSignature = op2.signature + if not self.is_subtype(sig1.left, sig2.left) or not self.is_subtype( + sig1.right, sig2.right + ): + best_match = False + break + self.logger.debug(f"{op1} is a full overload of {op2}") + if best_match: + return op1.result + + overloads: list[str] = [ + f"({op.signature.left} {op.signature.method} {op.signature.right}) -> {op.result}" + for op in valid_operations + ] + self.error( + expr.location, + f"Ambiguous operation {method} between {left} and {right}, multiple matching overloads: {', '.join(overloads)}", + ) + return UnknownType() def visit_compare_expr(self, expr: p.CompareExpr) -> Type: method: Optional[str] = COMPARATOR_METHODS.get(expr.operator.__class__) diff --git a/midas/checker/types.py b/midas/checker/types.py index c1d4449..83707b6 100644 --- a/midas/checker/types.py +++ b/midas/checker/types.py @@ -45,4 +45,16 @@ class ComplexType: properties: dict[str, Type] +@dataclass(frozen=True, kw_only=True) +class Operation: + signature: CallSignature + result: Type + + @dataclass(frozen=True, kw_only=True) + class CallSignature: + left: Type + method: str + right: Type + + Type = BaseType | AliasType | UnknownType | UnitType | Function | ComplexType diff --git a/midas/resolver/midas.py b/midas/resolver/midas.py index acbbe96..2962b09 100644 --- a/midas/resolver/midas.py +++ b/midas/resolver/midas.py @@ -3,6 +3,7 @@ from typing import Optional import midas.ast.midas as m from midas.checker.types import ( AliasType, + Operation, Type, UnknownType, ) @@ -14,7 +15,7 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T def __init__(self) -> None: self._types: dict[str, Type] = {} - self._operations: dict[tuple[Type, str, Type], Type] = {} + self._operations: dict[Operation.CallSignature, Type] = {} define_builtins(self) @@ -48,10 +49,26 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T Returns: Optional[Type]: the result type, or None if no matching operation was found """ - operation: tuple[Type, str, Type] = (left, operator, right) - result: Optional[Type] = self._operations.get(operation) + signature: Operation.CallSignature = Operation.CallSignature( + left=left, + method=operator, + right=right, + ) + result: Optional[Type] = self._operations.get(signature) return result + def get_operations_by_name(self, name: str) -> list[Operation]: + operations: list[Operation] = [] + for signature, result in self._operations.items(): + if signature.method == name: + operations.append( + Operation( + signature=signature, + result=result, + ) + ) + return operations + def define_type(self, name: str, type: Type) -> Type: """Define a type in the registry @@ -82,12 +99,16 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T Raises: ValueError: if an operation is already defined with these operands and name """ - operation: tuple[Type, str, Type] = (left, operator, right) - if operation in self._operations: + signature: Operation.CallSignature = Operation.CallSignature( + left=left, + method=operator, + right=right, + ) + if signature in self._operations: raise ValueError( f"Operation {operator} already defined between {left} and {right}" ) - self._operations[operation] = result + self._operations[signature] = result def resolve(self, stmts: list[m.Stmt]): """Process a sequence of statements -- 2.49.1 From 3e0dc60283d457b599dfbfbb3c6aea93cf682d32 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 13:59:27 +0200 Subject: [PATCH 10/15] fix(checker): only unfold alias on subtype --- midas/checker/checker.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index 9ed50f0..43948eb 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -178,13 +178,6 @@ class Checker( stmts: list[m.Stmt] = parser.parse() self.ctx.resolve(stmts) - def unfold_type(self, type: Type) -> Type: - match type: - case AliasType(type=ref_type): - return self.unfold_type(ref_type) - case _: - return type - def is_subtype(self, type1: Type, type2: Type) -> bool: """Check whether `type1` is a subtype of `type2` @@ -198,13 +191,13 @@ class Checker( bool: whether `type1` is a subtype of `type2` """ - type1 = self.unfold_type(type1) - type2 = self.unfold_type(type2) - if type1 == type2: return True match (type1, type2): + case (AliasType(type=base1), _): + return self.is_subtype(base1, type2) + case (BaseType(name=name1), BaseType(name=name2)): return name1 in BUILTIN_SUBTYPES.get(name2, set()) -- 2.49.1 From 59e73f0fd95b36f84f208c5edf1a49c3852b39cd Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 14:00:02 +0200 Subject: [PATCH 11/15] fix(checker): invert property subtype check --- midas/checker/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index 43948eb..57fa5e9 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -205,7 +205,7 @@ class Checker( for k, t in props2.items(): if k not in props1: return False - if self.is_subtype(props1[k], t): + if not self.is_subtype(props1[k], t): return False return True -- 2.49.1 From d278dc5f5b6d25ee56ab474e7e0238480d73f561 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 14:28:36 +0200 Subject: [PATCH 12/15] tests: update tests with operation overloads --- .../checker/02_simple_operations.py.ref.json | 18 ++-------- tests/cases/checker/06_subtyping.py.ref.json | 35 +++---------------- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/tests/cases/checker/02_simple_operations.py.ref.json b/tests/cases/checker/02_simple_operations.py.ref.json index 6952790..654af17 100644 --- a/tests/cases/checker/02_simple_operations.py.ref.json +++ b/tests/cases/checker/02_simple_operations.py.ref.json @@ -13,20 +13,6 @@ ] }, "message": "Cannot assign BaseType(name='str') to c of type BaseType(name='int')" - }, - { - "type": "Error", - "location": { - "start": [ - 9, - 4 - ], - "end": [ - 9, - 9 - ] - }, - "message": "Undefined operation __add__ between BaseType(name='bool') and BaseType(name='bool')" } ], "judgments": [ @@ -172,7 +158,9 @@ "name": "d" } }, - "type": {} + "type": { + "name": "int" + } }, { "location": { diff --git a/tests/cases/checker/06_subtyping.py.ref.json b/tests/cases/checker/06_subtyping.py.ref.json index c3482bc..689402e 100644 --- a/tests/cases/checker/06_subtyping.py.ref.json +++ b/tests/cases/checker/06_subtyping.py.ref.json @@ -1,34 +1,5 @@ { - "diagnostics": [ - { - "type": "Error", - "location": { - "start": [ - 12, - 5 - ], - "end": [ - 12, - 12 - ] - }, - "message": "Undefined operation __add__ between BaseType(name='int') and BaseType(name='float')" - }, - { - "type": "Error", - "location": { - "start": [ - 12, - 0 - ], - "end": [ - 12, - 12 - ] - }, - "message": "Cannot assign UnknownType() to v3 of type BaseType(name='float')" - } - ], + "diagnostics": [], "judgments": [ { "location": { @@ -214,7 +185,9 @@ "name": "v2" } }, - "type": {} + "type": { + "name": "float" + } } ] } \ No newline at end of file -- 2.49.1 From b8cb2b42734686a2de5c8de1e3730b8b988b3074 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 15:07:24 +0200 Subject: [PATCH 13/15] feat(checker): handle attribute getter --- .../04_complex_types.midas | 11 ++++++++ .../04_complex_types.py | 9 +++++++ midas/checker/checker.py | 26 ++++++++++++++++++- midas/resolver/midas.py | 10 ++++--- 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 examples/01_simple_type_checking/04_complex_types.midas create mode 100644 examples/01_simple_type_checking/04_complex_types.py diff --git a/examples/01_simple_type_checking/04_complex_types.midas b/examples/01_simple_type_checking/04_complex_types.midas new file mode 100644 index 0000000..b920c37 --- /dev/null +++ b/examples/01_simple_type_checking/04_complex_types.midas @@ -0,0 +1,11 @@ +type Meter = float + +extend Meter { + op __add__(Meter) -> Meter + op __sub__(Meter) -> Meter +} + +type Coordinate = { + x: Meter + y: Meter +} \ No newline at end of file diff --git a/examples/01_simple_type_checking/04_complex_types.py b/examples/01_simple_type_checking/04_complex_types.py new file mode 100644 index 0000000..476cb90 --- /dev/null +++ b/examples/01_simple_type_checking/04_complex_types.py @@ -0,0 +1,9 @@ +# type: ignore +# ruff: disable [F821] +p1: Coordinate +p2: Coordinate + +diff_x = p2.x - p1.x +diff_y = p2.y - p1.y + +dist = diff_x + diff_y diff --git a/midas/checker/checker.py b/midas/checker/checker.py index 57fa5e9..ad06b9c 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -178,6 +178,13 @@ class Checker( stmts: list[m.Stmt] = parser.parse() self.ctx.resolve(stmts) + def unfold_type(self, type: Type) -> Type: + match type: + case AliasType(type=ref_type): + return self.unfold_type(ref_type) + case _: + return type + def is_subtype(self, type1: Type, type2: Type) -> bool: """Check whether `type1` is a subtype of `type2` @@ -562,7 +569,24 @@ class Checker( ) return function.returns - def visit_get_expr(self, expr: p.GetExpr) -> Type: ... + def visit_get_expr(self, expr: p.GetExpr) -> Type: + object: Type = self.type_of(expr.object) + base_object: Type = self.unfold_type(object) + match base_object: + case ComplexType(properties=properties): + if expr.name not in properties: + self.error( + expr.location, f"Unknown property '{expr.name} on {object}" + ) + return UnknownType() + return properties[expr.name] + case UnknownType(): + return UnknownType() + case _: + self.error( + expr.location, f"Cannot get property '{expr.name}' on {object}" + ) + return UnknownType() def visit_literal_expr(self, expr: p.LiteralExpr) -> Type: match expr.value: diff --git a/midas/resolver/midas.py b/midas/resolver/midas.py index 2962b09..468f59a 100644 --- a/midas/resolver/midas.py +++ b/midas/resolver/midas.py @@ -3,6 +3,7 @@ from typing import Optional import midas.ast.midas as m from midas.checker.types import ( AliasType, + ComplexType, Operation, Type, UnknownType, @@ -178,7 +179,8 @@ class MidasResolver(m.Stmt.Visitor[None], m.Expr.Visitor[None], m.Type.Visitor[T return UnknownType() def visit_complex_type(self, type: m.ComplexType) -> Type: - for prop in type.properties: - prop.accept(self) - # TODO - return UnknownType() + return ComplexType( + properties={ + prop.name.lexeme: prop.type.accept(self) for prop in type.properties + } + ) -- 2.49.1 From e665d03533f5fb587672afd9586af2e3c843e2a6 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 17:48:31 +0200 Subject: [PATCH 14/15] fix: remove unused SetExpr --- gen/python.py | 6 ------ midas/ast/printer.py | 11 ----------- midas/ast/python.py | 13 ------------- midas/checker/checker.py | 2 -- midas/cli/highlighter.py | 2 -- midas/resolver/resolver.py | 4 ---- tests/serializer/python.py | 9 --------- 7 files changed, 47 deletions(-) diff --git a/gen/python.py b/gen/python.py index 09d21b8..e6d08c9 100644 --- a/gen/python.py +++ b/gen/python.py @@ -128,12 +128,6 @@ class LogicalExpr: right: Expr -class SetExpr: - object: Expr - name: str - value: Expr - - class CastExpr: type: MidasType expr: Expr diff --git a/midas/ast/printer.py b/midas/ast/printer.py index 41dd6a0..f8fb411 100644 --- a/midas/ast/printer.py +++ b/midas/ast/printer.py @@ -602,17 +602,6 @@ class PythonAstPrinter( with self._child_level(single=True): expr.right.accept(self) - def visit_set_expr(self, expr: p.SetExpr) -> None: - self._write_line("SetExpr") - with self._child_level(): - self._write_line("object") - with self._child_level(single=True): - expr.object.accept(self) - self._write_line(f"name: {expr.name}") - self._write_line("value", last=True) - with self._child_level(single=True): - expr.value.accept(self) - def visit_cast_expr(self, expr: p.CastExpr) -> None: self._write_line("CastExpr") with self._child_level(): diff --git a/midas/ast/python.py b/midas/ast/python.py index 8607cd2..dd5d905 100644 --- a/midas/ast/python.py +++ b/midas/ast/python.py @@ -214,9 +214,6 @@ class Expr(ABC): @abstractmethod def visit_logical_expr(self, expr: LogicalExpr) -> T: ... - @abstractmethod - def visit_set_expr(self, expr: SetExpr) -> T: ... - @abstractmethod def visit_cast_expr(self, expr: CastExpr) -> T: ... @@ -298,16 +295,6 @@ class LogicalExpr(Expr): return visitor.visit_logical_expr(self) -@dataclass(frozen=True) -class SetExpr(Expr): - object: Expr - name: str - value: Expr - - def accept(self, visitor: Expr.Visitor[T]) -> T: - return visitor.visit_set_expr(self) - - @dataclass(frozen=True) class CastExpr(Expr): type: MidasType diff --git a/midas/checker/checker.py b/midas/checker/checker.py index ad06b9c..4571ecc 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -620,8 +620,6 @@ class Checker( ) return UnknownType() - def visit_set_expr(self, expr: p.SetExpr) -> Type: ... - def visit_cast_expr(self, expr: p.CastExpr) -> Type: return expr.type.accept(self) diff --git a/midas/cli/highlighter.py b/midas/cli/highlighter.py index b1b705f..e4a9556 100644 --- a/midas/cli/highlighter.py +++ b/midas/cli/highlighter.py @@ -210,8 +210,6 @@ class PythonHighlighter( def visit_logical_expr(self, expr: p.LogicalExpr) -> None: ... - def visit_set_expr(self, expr: p.SetExpr) -> None: ... - def visit_cast_expr(self, expr: p.CastExpr) -> None: ... def visit_ternary_expr(self, expr: p.TernaryExpr) -> None: ... diff --git a/midas/resolver/resolver.py b/midas/resolver/resolver.py index 15166bd..31a46df 100644 --- a/midas/resolver/resolver.py +++ b/midas/resolver/resolver.py @@ -174,10 +174,6 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]): self.resolve(expr.left) self.resolve(expr.right) - def visit_set_expr(self, expr: p.SetExpr) -> None: - self.resolve(expr.value) - self.resolve(expr.object) - def visit_cast_expr(self, expr: p.CastExpr) -> None: self.resolve(expr.expr) diff --git a/tests/serializer/python.py b/tests/serializer/python.py index 786b15b..bab3f8c 100644 --- a/tests/serializer/python.py +++ b/tests/serializer/python.py @@ -20,7 +20,6 @@ from midas.ast.python import ( LogicalExpr, MidasType, ReturnStmt, - SetExpr, Stmt, TernaryExpr, TypeAssign, @@ -232,14 +231,6 @@ class PythonAstJsonSerializer( "right": expr.right.accept(self), } - def visit_set_expr(self, expr: SetExpr) -> dict: - return { - "_type": "SetExpr", - "object": expr.object.accept(self), - "name": expr.name, - "value": expr.value.accept(self), - } - def visit_cast_expr(self, expr: CastExpr) -> dict: return { "_type": "CastExpr", -- 2.49.1 From e0179bc44232e830f16de10e311b49dfcf4806cb Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Sun, 7 Jun 2026 17:50:56 +0200 Subject: [PATCH 15/15] feat(checker): handle assignments to attributes --- .../04_complex_types.py | 2 + midas/checker/checker.py | 72 +++++++++++++++---- midas/resolver/resolver.py | 5 +- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/examples/01_simple_type_checking/04_complex_types.py b/examples/01_simple_type_checking/04_complex_types.py index 476cb90..f36ef52 100644 --- a/examples/01_simple_type_checking/04_complex_types.py +++ b/examples/01_simple_type_checking/04_complex_types.py @@ -7,3 +7,5 @@ diff_x = p2.x - p1.x diff_y = p2.y - p1.y dist = diff_x + diff_y + +p2.x += cast(Meter, 1) diff --git a/midas/checker/checker.py b/midas/checker/checker.py index 4571ecc..ab7261c 100644 --- a/midas/checker/checker.py +++ b/midas/checker/checker.py @@ -437,24 +437,64 @@ class Checker( def visit_assign_stmt(self, stmt: p.AssignStmt) -> None: value_type: 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}") - self.warning(target.location, f"Unsupported assignment to {target}") - continue - name: str = target.name - var_type: Optional[Type] = self.look_up_variable(name, target) + self._assign(stmt.location, target, value_type) - if var_type is None: - self.env.define(name, value_type) - else: - # S <: T - # Γ, x: T v: S - # x = v - if not self.is_subtype(value_type, var_type): + def _assign(self, location: Location, target: p.Expr, value_type: Type): + match target: + case p.VariableExpr(): + self._assign_var(location, target, value_type) + + case p.GetExpr(): + self._assign_attr(location, target, value_type) + + case _: + if not isinstance(target, p.VariableExpr): + self.logger.warning(f"Unsupported assignment to {target}") + self.warning(target.location, f"Unsupported assignment to {target}") + + def _assign_var(self, location: Location, target: p.VariableExpr, value_type: Type): + name: str = target.name + var_type: Optional[Type] = self.look_up_variable(name, target) + + if var_type is None: + self.env.define(name, value_type) + else: + # S <: T + # Γ, x: T v: S + # x = v + if not self.is_subtype(value_type, var_type): + self.error( + location, + f"Cannot assign {value_type} to {name} of type {var_type}", + ) + + def _assign_attr(self, location: Location, target: p.GetExpr, value_type: Type): + object: Type = self.type_of(target.object) + base_object: Type = self.unfold_type(object) + match base_object: + case ComplexType(properties=properties): + if target.name not in properties: self.error( - stmt.location, - f"Cannot assign {value_type} to {name} of type {var_type}", + target.location, f"Unknown property '{target.name} on {object}" ) + return + + prop_type: Type = properties[target.name] + if not self.is_subtype(value_type, prop_type): + self.error( + location, + f"Cannot assign {value_type} to property '{target.name}' of type {prop_type} on {object}", + ) + return + + case UnknownType(): + pass + + case _: + self.error( + target.location, + f"Cannot assign {value_type} to unknown property '{target.name}' on {object}", + ) def visit_return_stmt(self, stmt: p.ReturnStmt) -> None: type: Type = stmt.value.accept(self) if stmt.value is not None else UnitType() @@ -580,8 +620,10 @@ class Checker( ) return UnknownType() return properties[expr.name] + case UnknownType(): return UnknownType() + case _: self.error( expr.location, f"Cannot get property '{expr.name}' on {object}" diff --git a/midas/resolver/resolver.py b/midas/resolver/resolver.py index 31a46df..18fcba4 100644 --- a/midas/resolver/resolver.py +++ b/midas/resolver/resolver.py @@ -111,9 +111,8 @@ class Resolver(p.Stmt.Visitor[None], p.Expr.Visitor[None]): self.resolve(stmt.value) for target in stmt.targets: match target: - case p.VariableExpr(name=name): - self.resolve_local(target, name) - # TODO: declare if not found + case p.VariableExpr() | p.GetExpr(): + target.accept(self) case _: raise Exception(f"Unsupported assignment to {target}") -- 2.49.1