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
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):
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()

View File

@@ -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 "<Unknown>"
@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):

View File

@@ -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": [

View File

@@ -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": [