feat(types): add human-friendly string rep
add `__str__` methods on type structures to improve readability of diagnostics
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
Reference in New Issue
Block a user