feat(types): add human-friendly string rep

add `__str__` methods on type structures to improve readability of diagnostics
This commit is contained in:
2026-06-09 12:59:36 +02:00
parent b8bb8190c4
commit efa5454776
5 changed files with 66 additions and 14 deletions

View File

@@ -9,3 +9,6 @@ diff_y = p2.y - p1.y
dist = diff_x + diff_y dist = diff_x + diff_y
p2.x += cast(Meter, 1) p2.x += cast(Meter, 1)
p2.y = True
p2.z = 3
p2.x.a = 3

View File

@@ -273,7 +273,7 @@ class PythonTyper(
if not self.is_subtype(value_type, var_type): if not self.is_subtype(value_type, var_type):
self.reporter.error( self.reporter.error(
location, 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): def _assign_attr(self, location: Location, target: p.GetExpr, value_type: Type):
@@ -283,7 +283,7 @@ class PythonTyper(
case ComplexType(properties=properties): case ComplexType(properties=properties):
if target.name not in properties: if target.name not in properties:
self.reporter.error( self.reporter.error(
target.location, f"Unknown property '{target.name} on {object}" target.location, f"Unknown property '{object}.{target.name}'"
) )
return return
@@ -291,7 +291,7 @@ class PythonTyper(
if not self.is_subtype(value_type, prop_type): if not self.is_subtype(value_type, prop_type):
self.reporter.error( self.reporter.error(
location, 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 return
@@ -301,7 +301,7 @@ class PythonTyper(
case _: case _:
self.reporter.error( self.reporter.error(
target.location, 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: def visit_return_stmt(self, stmt: p.ReturnStmt) -> None:
@@ -365,6 +365,9 @@ class PythonTyper(
if i == j: if i == j:
continue continue
sig2: Operation.CallSignature = op2.signature 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( if not self.is_subtype(sig1.left, sig2.left) or not self.is_subtype(
sig1.right, sig2.right sig1.right, sig2.right
): ):
@@ -374,13 +377,9 @@ class PythonTyper(
if best_match: if best_match:
return op1.result 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( self.reporter.error(
expr.location, 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() return UnknownType()

View File

@@ -8,21 +8,29 @@ from typing import Optional
class BaseType: class BaseType:
name: str name: str
def __str__(self) -> str:
return self.name
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class AliasType: class AliasType:
name: str name: str
type: Type type: Type
def __str__(self) -> str:
return self.name
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class UnknownType: class UnknownType:
pass def __str__(self) -> str:
return "<Unknown>"
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class UnitType: class UnitType:
pass def __str__(self) -> str:
return "None"
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@@ -33,6 +41,23 @@ class Function:
kw_args: list[Argument] kw_args: list[Argument]
returns: Type 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) @dataclass(frozen=True, kw_only=True)
class Argument: class Argument:
pos: int pos: int
@@ -40,29 +65,48 @@ class Function:
type: Type type: Type
required: bool 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) @dataclass(frozen=True, kw_only=True)
class ComplexType: class ComplexType:
properties: dict[str, Type] 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) @dataclass(frozen=True, kw_only=True)
class Operation: class Operation:
signature: CallSignature signature: CallSignature
result: Type result: Type
def __str__(self) -> str:
return f"{self.signature} -> {self.result}"
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class CallSignature: class CallSignature:
left: Type left: Type
method: str method: str
right: Type right: Type
def __str__(self) -> str:
return f"{self.method}({self.left}, {self.right})"
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class TypeVar: class TypeVar:
name: str name: str
bound: Optional[Type] 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) @dataclass(frozen=True, kw_only=True)
class GenericType: class GenericType:
@@ -70,6 +114,9 @@ class GenericType:
params: list[TypeVar] params: list[TypeVar]
body: Type body: Type
def __str__(self) -> str:
return f"{self.name}[{', '.join(map(str, self.params))}]"
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class AppliedType: class AppliedType:
@@ -77,6 +124,9 @@ class AppliedType:
args: list[Type] args: list[Type]
body: 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 substitute_typevars(type: Type, substitutions: dict[str, Type]) -> Type:
def sub_argument(arg: Function.Argument): def sub_argument(arg: Function.Argument):

View File

@@ -12,7 +12,7 @@
13 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": [ "judgments": [

View File

@@ -236,7 +236,7 @@
13 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", "type": "Error",
@@ -250,7 +250,7 @@
25 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": [ "judgments": [