Skip to content

Commit

Permalink
feat: Allow emission of diagnostics inside GuppyError (#553)
Browse files Browse the repository at this point in the history
Closes #538.

* Allow to pass a `Diagnostic` object to `GuppyError` instead of an
error message
* The old error reporting system remains untouched until we have
migrated everything to the new diagnostics system
* Adds a `TypeMismatchError` diagnostic as a test to ensure that the
pipeline works
  • Loading branch information
mark-koch authored Oct 18, 2024
1 parent f9709ae commit 3dc8b87
Show file tree
Hide file tree
Showing 21 changed files with 177 additions and 121 deletions.
31 changes: 20 additions & 11 deletions guppylang/checker/expr_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
import sys
import traceback
from contextlib import suppress
from dataclasses import replace
from typing import Any, NoReturn, cast
from dataclasses import dataclass, replace
from typing import Any, ClassVar, NoReturn, cast

from guppylang.ast_util import (
AstNode,
Expand All @@ -51,6 +51,7 @@
from guppylang.definition.module import ModuleDef
from guppylang.definition.ty import TypeDef
from guppylang.definition.value import CallableDef, ValueDef
from guppylang.diagnostic import Error
from guppylang.error import (
GuppyError,
GuppyTypeError,
Expand Down Expand Up @@ -135,6 +136,18 @@
} # fmt: skip


@dataclass(frozen=True)
class TypeMismatchError(Error):
"""Error diagnostic for expressions with the wrong type."""

expected: Type
actual: Type
kind: str = "expression"

title: ClassVar[str] = "Type mismatch"
span_label: ClassVar[str] = "Expected {kind} of type `{expected}`, got `{actual}`"


class ExprChecker(AstVisitor[tuple[ast.expr, Subst]]):
"""Checks an expression against a type and produces a new type-annotated AST.
Expand Down Expand Up @@ -164,9 +177,7 @@ def _fail(
_, actual = self._synthesize(actual, allow_free_vars=True)
if loc is None:
raise InternalGuppyError("Failure location is required")
raise GuppyTypeError(
f"Expected {self._kind} of type `{expected}`, got `{actual}`", loc
)
raise GuppyTypeError(TypeMismatchError(loc, expected, actual))

def check(
self, expr: ast.expr, ty: Type, kind: str = "expression"
Expand Down Expand Up @@ -731,7 +742,7 @@ def check_type_against(
unquantified, free_vars = act.unquantified()
subst = unify(exp, unquantified, {})
if subst is None:
raise GuppyTypeError(f"Expected {kind} of type `{exp}`, got `{act}`", node)
raise GuppyTypeError(TypeMismatchError(node, exp, act, kind))
# Check that we have found a valid instantiation for all params
for i, v in enumerate(free_vars):
if v not in subst:
Expand Down Expand Up @@ -760,7 +771,7 @@ def check_type_against(
assert not act.unsolved_vars
subst = unify(exp, act, {})
if subst is None:
raise GuppyTypeError(f"Expected {kind} of type `{exp}`, got `{act}`", node)
raise GuppyTypeError(TypeMismatchError(node, exp, act, kind))
return subst, []


Expand Down Expand Up @@ -927,17 +938,15 @@ def check_call(
synth, inst = res
subst = unify(ty, synth, {})
if subst is None:
raise GuppyTypeError(f"Expected {kind} of type `{ty}`, got `{synth}`", node)
raise GuppyTypeError(TypeMismatchError(node, ty, synth, kind))
return inputs, subst, inst

# If synthesis fails, we try again, this time also using information from the
# expected return type
unquantified, free_vars = func_ty.unquantified()
subst = unify(ty, unquantified.output, {})
if subst is None:
raise GuppyTypeError(
f"Expected {kind} of type `{ty}`, got `{unquantified.output}`", node
)
raise GuppyTypeError(TypeMismatchError(node, ty, unquantified.output, kind))

# Try to infer more by checking against the arguments
inputs, subst = type_check_args(inputs, unquantified, subst, ctx, node)
Expand Down
10 changes: 10 additions & 0 deletions guppylang/diagnostic.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ def add_sub_diagnostic(self, sub: "SubDiagnostic") -> Self:
self.children.append(sub)
return self

@property
def rendered_message(self) -> str | None:
"""The message of this diagnostic with formatted placeholders if provided."""
return self._render(self.message)

@property
def rendered_span_label(self) -> str | None:
"""The span label of this diagnostic with formatted placeholders if provided."""
return self._render(self.span_label)


@runtime_checkable
@dataclass(frozen=True)
Expand Down
20 changes: 18 additions & 2 deletions guppylang/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
from contextlib import contextmanager
from dataclasses import dataclass, field
from types import TracebackType
from typing import Any, TypeVar, cast
from typing import TYPE_CHECKING, Any, TypeVar, cast

from guppylang.ast_util import AstNode, get_file, get_line_offset, get_source
from guppylang.ipython_inspect import is_running_ipython

if TYPE_CHECKING:
from guppylang.diagnostic import Diagnostic


@dataclass(frozen=True)
class SourceLoc:
Expand Down Expand Up @@ -49,7 +52,7 @@ class GuppyError(Exception):
The error message can also refer to AST locations using format placeholders `{0}`,
`{1}`, etc. and passing the corresponding AST nodes to `locs_in_msg`."""

raw_msg: str
raw_msg: "str | Diagnostic"
location: AstNode | None = None
# The message can also refer to AST locations using format placeholders `{0}`, `{1}`
locs_in_msg: Sequence[AstNode | None] = field(default_factory=list)
Expand All @@ -59,6 +62,7 @@ def get_msg(self) -> str:
A line offset is needed to translate AST locations mentioned in the message into
source locations in the actual file."""
assert isinstance(self.raw_msg, str)
return self.raw_msg.format(
*(
SourceLoc.from_ast(loc) if loc is not None else "???"
Expand Down Expand Up @@ -157,6 +161,18 @@ def hook(
excty: type[BaseException], err: BaseException, traceback: TracebackType | None
) -> None:
"""Custom `excepthook` that intercepts `GuppyExceptions` for pretty printing."""
from guppylang.diagnostic import Diagnostic, DiagnosticsRenderer

# Check for errors using our new diagnostics system
if isinstance(err, GuppyError) and isinstance(err.raw_msg, Diagnostic):
from guppylang.decorator import guppy

renderer = DiagnosticsRenderer(guppy._sources)
renderer.render_diagnostic(err.raw_msg)
sys.stderr.write("\n".join(renderer.buffer))
sys.stderr.write("\n\nGuppy compilation failed due to 1 previous error\n")
return

# Fall back to default hook if it's not a GuppyException or we're missing an
# error location
if not isinstance(err, GuppyError) or err.location is None:
Expand Down
13 changes: 7 additions & 6 deletions tests/error/array_errors/new_array_elem_mismatch2.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:13
Error: Type mismatch (at $FILE:13:13)
|
11 | @guppy(module)
12 | def main() -> None:
13 | array(1, False)
| ^^^^^ Expected expression of type `int`, got `bool`

11: @guppy(module)
12: def main() -> None:
13: array(1, False)
^^^^^
GuppyTypeError: Expected expression of type `int`, got `bool`
Guppy compilation failed due to 1 previous error
14 changes: 8 additions & 6 deletions tests/error/inout_errors/conflict.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Guppy compilation failed. Error in file $FILE:19
Error: Type mismatch (at $FILE:19:11)
|
17 | @guppy(module)
18 | def test() -> Callable[[qubit @owned], qubit]:
19 | return foo
| ^^^ Expected return value of type `qubit @owned -> qubit`, got
| `qubit -> qubit`

17: @guppy(module)
18: def test() -> Callable[[qubit @owned], qubit]:
19: return foo
^^^
GuppyTypeError: Expected return value of type `qubit @owned -> qubit`, got `qubit -> qubit`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/misc_errors/implicit_module_error.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:6
Error: Type mismatch (at $FILE:6:11)
|
4 | @guppy
5 | def foo() -> int:
6 | return 1.0
| ^^^ Expected return value of type `int`, got `float`

4: @guppy
5: def foo() -> int:
6: return 1.0
^^^
GuppyTypeError: Expected return value of type `int`, got `float`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/poly_errors/arg_mismatch1.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:17
Error: Type mismatch (at $FILE:17:11)
|
15 | @guppy(module)
16 | def main(x: bool, y: tuple[bool]) -> None:
17 | foo(x, y)
| ^ Expected argument of type `bool`, got `(bool)`

15: @guppy(module)
16: def main(x: bool, y: tuple[bool]) -> None:
17: foo(x, y)
^
GuppyTypeError: Expected argument of type `bool`, got `(bool)`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/poly_errors/arg_mismatch2.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:17
Error: Type mismatch (at $FILE:17:8)
|
15 | @guppy(module)
16 | def main() -> None:
17 | foo(False)
| ^^^^^ Expected argument of type `(?T, ?T)`, got `bool`

15: @guppy(module)
16: def main() -> None:
17: foo(False)
^^^^^
GuppyTypeError: Expected argument of type `(?T, ?T)`, got `bool`
Guppy compilation failed due to 1 previous error
14 changes: 8 additions & 6 deletions tests/error/poly_errors/arg_mismatch3.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Guppy compilation failed. Error in file $FILE:17
Error: Type mismatch (at $FILE:17:11)
|
15 | @guppy(module)
16 | def main(x: array[int, 42], y: array[int, 43]) -> None:
17 | foo(x, y)
| ^ Expected argument of type `array[int, 42]`, got `array[int,
| 43]`

15: @guppy(module)
16: def main(x: array[int, 42], y: array[int, 43]) -> None:
17: foo(x, y)
^
GuppyTypeError: Expected argument of type `array[int, 42]`, got `array[int, 43]`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/poly_errors/inst_return_mismatch.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:17
Error: Type mismatch (at $FILE:17:14)
|
15 | @guppy(module)
16 | def main(x: bool) -> None:
17 | y: None = foo(x)
| ^^^^^^ Expected expression of type `None`, got `bool`

15: @guppy(module)
16: def main(x: bool) -> None:
17: y: None = foo(x)
^^^^^^
GuppyTypeError: Expected expression of type `None`, got `bool`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/poly_errors/inst_return_mismatch_nested.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:17
Error: Type mismatch (at $FILE:17:14)
|
15 | @guppy(module)
16 | def main(x: bool) -> None:
17 | y: None = foo(foo(foo(x)))
| ^^^^^^^^^^^^^^^^ Expected expression of type `None`, got `bool`

15: @guppy(module)
16: def main(x: bool) -> None:
17: y: None = foo(foo(foo(x)))
^^^^^^^^^^^^^^^^
GuppyTypeError: Expected expression of type `None`, got `bool`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/poly_errors/return_mismatch.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:17
Error: Type mismatch (at $FILE:17:14)
|
15 | @guppy(module)
16 | def main() -> None:
17 | x: bool = foo()
| ^^^^^ Expected expression of type `bool`, got `(?T, ?T)`

15: @guppy(module)
16: def main() -> None:
17: x: bool = foo()
^^^^^
GuppyTypeError: Expected expression of type `bool`, got `(?T, ?T)`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/struct_errors/constructor_arg_mismatch.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:15
Error: Type mismatch (at $FILE:15:13)
|
13 | @guppy(module)
14 | def main() -> None:
15 | MyStruct(0)
| ^ Expected argument of type `(int, int)`, got `int`

13: @guppy(module)
14: def main() -> None:
15: MyStruct(0)
^
GuppyTypeError: Expected argument of type `(int, int)`, got `int`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/struct_errors/constructor_arg_mismatch_poly.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:19
Error: Type mismatch (at $FILE:19:16)
|
17 | @guppy(module)
18 | def main() -> None:
19 | MyStruct(0, False)
| ^^^^^ Expected argument of type `int`, got `bool`

17: @guppy(module)
18: def main() -> None:
19: MyStruct(0, False)
^^^^^
GuppyTypeError: Expected argument of type `int`, got `bool`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/tensor_errors/type_mismatch.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:12
Error: Type mismatch (at $FILE:12:26)
|
10 | @guppy(module)
11 | def main() -> int:
12 | return (foo, foo)(42, False)
| ^^^^^ Expected argument of type `int`, got `bool`

10: @guppy(module)
11: def main() -> int:
12: return (foo, foo)(42, False)
^^^^^
GuppyTypeError: Expected argument of type `int`, got `bool`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/type_errors/call_wrong_arg.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:6
Error: Type mismatch (at $FILE:6:15)
|
4 | @compile_guppy
5 | def foo(x: int) -> int:
6 | return foo(True)
| ^^^^ Expected argument of type `int`, got `bool`

4: @compile_guppy
5: def foo(x: int) -> int:
6: return foo(True)
^^^^
GuppyTypeError: Expected argument of type `int`, got `bool`
Guppy compilation failed due to 1 previous error
14 changes: 8 additions & 6 deletions tests/error/type_errors/fun_ty_mismatch_1.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Guppy compilation failed. Error in file $FILE:11
Error: Type mismatch (at $FILE:11:11)
|
9 | return x > 0
10 |
11 | return bar
| ^^^ Expected return value of type `int -> int`, got `int ->
| bool`

9: return x > 0
10:
11: return bar
^^^
GuppyTypeError: Expected return value of type `int -> int`, got `int -> bool`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/type_errors/fun_ty_mismatch_2.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:11
Error: Type mismatch (at $FILE:11:15)
|
9 | return f()
10 |
11 | return bar(foo)
| ^^^ Expected argument of type `() -> int`, got `int -> int`

9: return f()
10:
11: return bar(foo)
^^^
GuppyTypeError: Expected argument of type `() -> int`, got `int -> int`
Guppy compilation failed due to 1 previous error
13 changes: 7 additions & 6 deletions tests/error/type_errors/fun_ty_mismatch_3.err
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
Guppy compilation failed. Error in file $FILE:11
Error: Type mismatch (at $FILE:11:15)
|
9 | return f(42)
10 |
11 | return bar(foo)
| ^^^ Expected argument of type `int -> bool`, got `int -> int`

9: return f(42)
10:
11: return bar(foo)
^^^
GuppyTypeError: Expected argument of type `int -> bool`, got `int -> int`
Guppy compilation failed due to 1 previous error
Loading

0 comments on commit 3dc8b87

Please sign in to comment.