From efa54547767287390a0010aa043cfb6989104ee3 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Tue, 9 Jun 2026 12:59:36 +0200 Subject: [PATCH] feat(types): add human-friendly string rep add `__str__` methods on type structures to improve readability of diagnostics --- .../04_complex_types.py | 3 ++ midas/checker/python.py | 17 +++--- midas/checker/types.py | 54 ++++++++++++++++++- .../checker/02_simple_operations.py.ref.json | 2 +- tests/cases/checker/03_functions.py.ref.json | 4 +- 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/examples/01_simple_type_checking/04_complex_types.py b/examples/01_simple_type_checking/04_complex_types.py index f36ef52..63fd1e7 100644 --- a/examples/01_simple_type_checking/04_complex_types.py +++ b/examples/01_simple_type_checking/04_complex_types.py @@ -9,3 +9,6 @@ diff_y = p2.y - p1.y dist = diff_x + diff_y p2.x += cast(Meter, 1) +p2.y = True +p2.z = 3 +p2.x.a = 3 diff --git a/midas/checker/python.py b/midas/checker/python.py index 59f6881..c11ee22 100644 --- a/midas/checker/python.py +++ b/midas/checker/python.py @@ -273,7 +273,7 @@ class PythonTyper( if not self.is_subtype(value_type, var_type): self.reporter.error( location, - f"Cannot assign {value_type} to {name} of type {var_type}", + f"Cannot assign {value_type} to variable '{name}' of type {var_type}", ) def _assign_attr(self, location: Location, target: p.GetExpr, value_type: Type): @@ -283,7 +283,7 @@ class PythonTyper( case ComplexType(properties=properties): if target.name not in properties: self.reporter.error( - target.location, f"Unknown property '{target.name} on {object}" + target.location, f"Unknown property '{object}.{target.name}'" ) return @@ -291,7 +291,7 @@ class PythonTyper( if not self.is_subtype(value_type, prop_type): self.reporter.error( location, - f"Cannot assign {value_type} to property '{target.name}' of type {prop_type} on {object}", + f"Cannot assign {value_type} to property '{object}.{target.name}' of type {prop_type}", ) return @@ -301,7 +301,7 @@ class PythonTyper( case _: self.reporter.error( target.location, - f"Cannot assign {value_type} to unknown property '{target.name}' on {object}", + f"Cannot assign {value_type} to unknown property '{object}.{target.name}'", ) def visit_return_stmt(self, stmt: p.ReturnStmt) -> None: @@ -365,6 +365,9 @@ class PythonTyper( if i == j: continue sig2: Operation.CallSignature = op2.signature + + # If op1 is not a full overload of op2 (i.e. operands of op1 are subtypes of op2's) + # ambiguity -> not best match if not self.is_subtype(sig1.left, sig2.left) or not self.is_subtype( sig1.right, sig2.right ): @@ -374,13 +377,9 @@ class PythonTyper( 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.reporter.error( expr.location, - f"Ambiguous operation {method} between {left} and {right}, multiple matching overloads: {', '.join(overloads)}", + f"Ambiguous operation {method} between {left} and {right}, multiple matching overloads: {', '.join(map(str, valid_operations))}", ) return UnknownType() diff --git a/midas/checker/types.py b/midas/checker/types.py index 9081a95..41ad786 100644 --- a/midas/checker/types.py +++ b/midas/checker/types.py @@ -8,21 +8,29 @@ from typing import Optional class BaseType: name: str + def __str__(self) -> str: + return self.name + @dataclass(frozen=True, kw_only=True) class AliasType: name: str type: Type + def __str__(self) -> str: + return self.name + @dataclass(frozen=True, kw_only=True) class UnknownType: - pass + def __str__(self) -> str: + return "" @dataclass(frozen=True, kw_only=True) class UnitType: - pass + def __str__(self) -> str: + return "None" @dataclass(frozen=True, kw_only=True) @@ -33,6 +41,23 @@ class Function: kw_args: list[Argument] returns: Type + def __str__(self) -> str: + args: list[str] = [] + if len(self.pos_args) != 0: + args += list(map(str, self.pos_args)) + if len(self.args) + len(self.kw_args) != 0: + args.append("/") + + if len(self.args) != 0: + args += list(map(str, self.args)) + + if len(self.kw_args) != 0: + if len(args) != 0: + args.append("*") + args += list(map(str, self.kw_args)) + + return f"{self.name}({', '.join(args)}) -> {self.returns}" + @dataclass(frozen=True, kw_only=True) class Argument: pos: int @@ -40,29 +65,48 @@ class Function: type: Type required: bool + def __str__(self) -> str: + opt: str = "" if self.required else "?" + return f"{self.name}: {self.type}{opt}" + @dataclass(frozen=True, kw_only=True) class ComplexType: properties: dict[str, Type] + def __str__(self) -> str: + props: list[str] = [f"{name}: {type}" for name, type in self.properties.items()] + return f"{{{', '.join(props)}}}" + @dataclass(frozen=True, kw_only=True) class Operation: signature: CallSignature result: Type + def __str__(self) -> str: + return f"{self.signature} -> {self.result}" + @dataclass(frozen=True, kw_only=True) class CallSignature: left: Type method: str right: Type + def __str__(self) -> str: + return f"{self.method}({self.left}, {self.right})" + @dataclass(frozen=True, kw_only=True) class TypeVar: name: str bound: Optional[Type] + def __str__(self) -> str: + if self.bound is not None: + return f"{self.name} <: {self.bound}" + return self.name + @dataclass(frozen=True, kw_only=True) class GenericType: @@ -70,6 +114,9 @@ class GenericType: params: list[TypeVar] body: Type + def __str__(self) -> str: + return f"{self.name}[{', '.join(map(str, self.params))}]" + @dataclass(frozen=True, kw_only=True) class AppliedType: @@ -77,6 +124,9 @@ class AppliedType: args: list[Type] body: Type + def __str__(self) -> str: + return f"{self.name}[{', '.join(map(str, self.args))}]" + def substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type: def sub_argument(arg: Function.Argument): diff --git a/tests/cases/checker/02_simple_operations.py.ref.json b/tests/cases/checker/02_simple_operations.py.ref.json index 654af17..e3881e0 100644 --- a/tests/cases/checker/02_simple_operations.py.ref.json +++ b/tests/cases/checker/02_simple_operations.py.ref.json @@ -12,7 +12,7 @@ 13 ] }, - "message": "Cannot assign BaseType(name='str') to c of type BaseType(name='int')" + "message": "Cannot assign str to variable 'c' of type int" } ], "judgments": [ diff --git a/tests/cases/checker/03_functions.py.ref.json b/tests/cases/checker/03_functions.py.ref.json index cd0ce42..3442bca 100644 --- a/tests/cases/checker/03_functions.py.ref.json +++ b/tests/cases/checker/03_functions.py.ref.json @@ -236,7 +236,7 @@ 13 ] }, - "message": "Wrong type for argument 'a', expected BaseType(name='int'), got BaseType(name='str')" + "message": "Wrong type for argument 'a', expected int, got str" }, { "type": "Error", @@ -250,7 +250,7 @@ 25 ] }, - "message": "Wrong type for argument 'c', expected BaseType(name='str'), got BaseType(name='bool')" + "message": "Wrong type for argument 'c', expected str, got bool" } ], "judgments": [