Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update statement checker to use new diagnostics #621

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 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 @@ -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
58 changes: 33 additions & 25 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,41 @@ 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,
)
raise GuppyTypeError(AttributeNotFoundError(attr_span, 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 +108,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 +123,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 +139,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
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
| ^ Attribute `z` not found on type `int`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be an easier read if this error was:

Suggested change
| ^ Attribute `z` not found on type `int`
| ^ Attribute `z` of type `int` not found

or even better

Suggested change
| ^ Attribute `z` not found on type `int`
| ^ `MyStruct` has no attribute `z` of type `int`

I actually don't see the relevance of the inferred type here, the error message on the left is quite descriptive as is. Perhaps I am missing some context here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops yes it's supposed to say "MyStruct" instead of "int" 👍

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
Loading