Skip to content

Commit

Permalink
feat: Update statement checker to use new diagnostics (#621)
Browse files Browse the repository at this point in the history
Closes #540
  • Loading branch information
mark-koch authored Nov 5, 2024
1 parent 08af12c commit cbe0830
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 70 deletions.
53 changes: 49 additions & 4 deletions guppylang/checker/errors/type_errors.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import ClassVar
from typing import TYPE_CHECKING, ClassVar

from guppylang.diagnostic import Error, Help, Note
from guppylang.tys.const import Const
from guppylang.tys.ty import FunctionType, Type

if TYPE_CHECKING:
from guppylang.definition.struct import StructField
from guppylang.tys.const import Const
from guppylang.tys.ty import FunctionType, Type


@dataclass(frozen=True)
Expand Down Expand Up @@ -33,6 +38,17 @@ class CantInstantiateFreeVars(Note):
illegal_inst: Type | Const


@dataclass(frozen=True)
class AssignFieldTypeMismatchError(Error):
title: ClassVar[str] = "Type mismatch"
span_label: ClassVar[str] = (
"Cannot assign expression of type `{actual}` to field `{field.name}` of type "
"`{field.ty}`"
)
actual: Type
field: StructField


@dataclass(frozen=True)
class TypeInferenceError(Error):
title: ClassVar[str] = "Cannot infer type"
Expand Down Expand Up @@ -60,7 +76,7 @@ class ModuleMemberNotFoundError(Error):
@dataclass(frozen=True)
class AttributeNotFoundError(Error):
title: ClassVar[str] = "Attribute not found"
span_label: ClassVar[str] = "Attribute `{attribute}` not found on type `{ty}`"
span_label: ClassVar[str] = "`{ty}` has no attribute `{attribute}`"
ty: Type
attribute: str

Expand Down Expand Up @@ -140,3 +156,32 @@ def rendered_span_label(self) -> str:
class SignatureHint(Note):
message: ClassVar[str] = "Function signature is `{sig}`"
sig: FunctionType


@dataclass(frozen=True)
class WrongNumberOfUnpacksError(Error):
title: ClassVar[str] = "{prefix} values to unpack"
expected: int
actual: int

@property
def prefix(self) -> str:
return "Not enough" if self.expected > self.actual else "Too many"

@property
def rendered_span_label(self) -> str:
diff = self.expected - self.actual
if diff < 0:
msg = "Unexpected assignment " + ("targets" if diff < -1 else "target")
else:
msg = "Not enough assignment targets"
return f"{msg} (expected {self.expected}, got {self.actual})"


@dataclass(frozen=True)
class AssignNonPlaceHelp(Help):
message: ClassVar[str] = (
"Consider assigning this value to a local variable first before assigning the "
"field `{field.name}`"
)
field: StructField
56 changes: 33 additions & 23 deletions guppylang/checker/stmt_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@

from guppylang.ast_util import AstVisitor, with_loc, with_type
from guppylang.cfg.bb import BB, BBStatement
from guppylang.checker.core import Context, FieldAccess, Variable
from guppylang.checker.core import Context, FieldAccess, UnsupportedError, Variable
from guppylang.checker.errors.type_errors import (
AssignFieldTypeMismatchError,
AssignNonPlaceHelp,
AttributeNotFoundError,
WrongNumberOfUnpacksError,
)
from guppylang.checker.expr_checker import ExprChecker, ExprSynthesizer
from guppylang.error import GuppyError, GuppyTypeError, InternalGuppyError
from guppylang.nodes import NestedFunctionDef, PlaceNode
from guppylang.span import Span, to_span
from guppylang.tys.parsing import type_from_ast
from guppylang.tys.subst import Subst
from guppylang.tys.ty import NoneType, StructType, TupleType, Type
Expand Down Expand Up @@ -57,40 +64,43 @@ def _check_assign(self, lhs: ast.expr, ty: Type, node: ast.stmt) -> ast.expr:

# The LHS could also be a field `expr.field`
case ast.Attribute(value=value, attr=attr):
# Unfortunately, the `attr` is just a string, not an AST node, so we
# have to compute its span by hand. This is fine since linebreaks are
# not allowed in the identifier following the `.`
span = to_span(lhs)
attr_span = Span(span.end.shift_left(len(attr)), span.end)
value, struct_ty = self._synth_expr(value)
if (
not isinstance(struct_ty, StructType)
or attr not in struct_ty.field_dict
):
raise GuppyTypeError(
f"Expression of type `{struct_ty}` has no attribute `{attr}`",
# Unfortunately, `attr` doesn't contain source annotations, so
# we have to use `lhs` as the error location
lhs,
AttributeNotFoundError(attr_span, struct_ty, attr)
)
field = struct_ty.field_dict[attr]
# TODO: In the future, we could infer some type args here
if field.ty != ty:
# TODO: Get hold of a span for the RHS and use a regular
# `TypeMismatchError` instead (maybe with a custom hint).
raise GuppyTypeError(
f"Cannot assign expression of type `{ty}` to field with type "
f"`{field.ty}`",
lhs,
AssignFieldTypeMismatchError(attr_span, ty, field)
)
if not isinstance(value, PlaceNode):
# For now we complain if someone tries to assign to something that
# is not a place, e.g. `f().a = 4`. This would only make sense if
# there is another reference to the return value of `f`, otherwise
# the mutation cannot be observed. We can start supporting this once
# we have proper reference semantics.
raise GuppyError(
"Assigning to this expression is not supported yet. Consider "
"binding the expression to variable and mutate that variable "
"instead.",
value,
err = UnsupportedError(
value, "Assigning to this expression", singular=True
)
err.add_sub_diagnostic(AssignNonPlaceHelp(None, field))
raise GuppyError(err)
if not field.ty.linear:
raise GuppyError(
"Mutation of classical fields is not supported yet", lhs
UnsupportedError(
attr_span, "Mutation of classical fields", singular=True
)
)
place = FieldAccess(value.place, struct_ty.field_dict[attr], lhs)
return with_loc(lhs, with_type(ty, PlaceNode(place=place)))
Expand All @@ -100,11 +110,11 @@ def _check_assign(self, lhs: ast.expr, ty: Type, node: ast.stmt) -> ast.expr:
tys = ty.element_types if isinstance(ty, TupleType) else [ty]
n, m = len(elts), len(tys)
if n != m:
raise GuppyTypeError(
f"{'Too many' if n < m else 'Not enough'} values to unpack "
f"(expected {n}, got {m})",
node,
)
if n > m:
span = Span(to_span(elts[m]).start, to_span(elts[-1]).end)
else:
span = to_span(lhs)
raise GuppyTypeError(WrongNumberOfUnpacksError(span, m, n))
lhs.elts = [
self._check_assign(pat, el_ty, node)
for pat, el_ty in zip(elts, tys, strict=True)
Expand All @@ -115,7 +125,9 @@ def _check_assign(self, lhs: ast.expr, ty: Type, node: ast.stmt) -> ast.expr:
# `a, *b = ...`. The former would require some runtime checks but
# the latter should be easier to do (unpack and repack the rest).
case _:
raise GuppyError("Assignment pattern not supported", lhs)
raise GuppyError(
UnsupportedError(lhs, "This assignment pattern", singular=True)
)

def visit_Assign(self, node: ast.Assign) -> ast.Assign:
if len(node.targets) > 1:
Expand All @@ -129,9 +141,7 @@ def visit_Assign(self, node: ast.Assign) -> ast.Assign:

def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.stmt:
if node.value is None:
raise GuppyError(
"Variable declaration is not supported. Assignment is required", node
)
raise GuppyError(UnsupportedError(node, "Variable declarations"))
ty = type_from_ast(node.annotation, self.ctx.globals)
node.value, subst = self._check_expr(node.value, ty)
assert not ty.unsolved_vars # `ty` must be closed!
Expand Down
16 changes: 10 additions & 6 deletions tests/error/struct_errors/assign_call.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
Guppy compilation failed. Error in file $FILE:20
Error: Unsupported (at $FILE:20:4)
|
18 | @guppy(module)
19 | def bar() -> None:
20 | foo().x += 1
| ^^^^^ Assigning to this expression is not supported

18: @guppy(module)
19: def bar() -> None:
20: foo().x += 1
^^^^^
GuppyError: Assigning to this expression is not supported yet. Consider binding the expression to variable and mutate that variable instead.
Help: Consider assigning this value to a local variable first before assigning
the field `x`

Guppy compilation failed due to 1 previous error
2 changes: 1 addition & 1 deletion tests/error/struct_errors/invalid_attribute_access.err
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ Error: Attribute not found (at $FILE:15:6)
13 | @guppy(module)
14 | def foo(s: MyStruct) -> None:
15 | s.z
| ^ Attribute `z` not found on type `MyStruct`
| ^ `MyStruct` has no attribute `z`

Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/struct_errors/invalid_attribute_assign1.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:15
Error: Attribute not found (at $FILE:15:6)
|
13 | @guppy(module)
14 | def foo(s: MyStruct) -> None:
15 | s.z = 2
| ^ `MyStruct` has no attribute `z`

13: @guppy(module)
14: def foo(s: MyStruct) -> None:
15: s.z = 2
^^^
GuppyTypeError: Expression of type `MyStruct` has no attribute `z`
Guppy compilation failed due to 1 previous error
14 changes: 8 additions & 6 deletions tests/error/struct_errors/invalid_attribute_assign2.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Guppy compilation failed. Error in file $FILE:15
Error: Type mismatch (at $FILE:15:6)
|
13 | @guppy(module)
14 | def foo(s: MyStruct) -> None:
15 | s.x = (1, 2)
| ^ Cannot assign expression of type `(int, int)` to field `x`
| of type `int`

13: @guppy(module)
14: def foo(s: MyStruct) -> None:
15: s.x = (1, 2)
^^^
GuppyTypeError: Cannot assign expression of type `(int, int)` to field with type `int`
Guppy compilation failed due to 1 previous error
14 changes: 8 additions & 6 deletions tests/error/struct_errors/invalid_attribute_assign3.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Guppy compilation failed. Error in file $FILE:15
Error: Type mismatch (at $FILE:15:6)
|
13 | @guppy(module)
14 | def foo(s: MyStruct) -> None:
15 | s.x, a = (1, 2), 3
| ^ Cannot assign expression of type `(int, int)` to field `x`
| of type `int`

13: @guppy(module)
14: def foo(s: MyStruct) -> None:
15: s.x, a = (1, 2), 3
^^^
GuppyTypeError: Cannot assign expression of type `(int, int)` to field with type `int`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/struct_errors/mutate_classical.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:18
Error: Unsupported (at $FILE:18:6)
|
16 | def foo(s: MyStruct) -> tuple[MyStruct, bool]:
17 | t = s
18 | t.x += 1
| ^ Mutation of classical fields is not supported

16: def foo(s: MyStruct) -> tuple[MyStruct, bool]:
17: t = s
18: t.x += 1
^^^
GuppyError: Mutation of classical fields is not supported yet
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/type_errors/unpack_not_enough.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:6
Error: Too many values to unpack (at $FILE:6:10)
|
4 | @compile_guppy
5 | def foo() -> int:
6 | a, b, c = 1, True
| ^ Unexpected assignment target (expected 2, got 3)

4: @compile_guppy
5: def foo() -> int:
6: a, b, c = 1, True
^^^^^^^^^^^^^^^^^
GuppyTypeError: Not enough values to unpack (expected 3, got 2)
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/type_errors/unpack_too_many.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:6
Error: Not enough values to unpack (at $FILE:6:4)
|
4 | @compile_guppy
5 | def foo() -> int:
6 | a, b = 1, True, 3.0
| ^^^^ Not enough assignment targets (expected 3, got 2)

4: @compile_guppy
5: def foo() -> int:
6: a, b = 1, True, 3.0
^^^^^^^^^^^^^^^^^^^
GuppyTypeError: Too many values to unpack (expected 2, got 3)
Guppy compilation failed due to 1 previous error

0 comments on commit cbe0830

Please sign in to comment.