From 37e1633fb32fc16d678fa84a02b961ec947feb91 Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Mon, 7 Oct 2024 11:00:15 +0100 Subject: [PATCH 01/20] Init From d671bb5ae45b8a88cc5a38cd60d6497710a02c50 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:44:35 +0100 Subject: [PATCH 02/20] feat: Add span and diagnostics base classes (#548) Closes #536. --- guppylang/diagnostic.py | 111 ++++++++++++++++++++++++++++++++++++++++ guppylang/span.py | 82 +++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 guppylang/diagnostic.py create mode 100644 guppylang/span.py diff --git a/guppylang/diagnostic.py b/guppylang/diagnostic.py new file mode 100644 index 00000000..92f75585 --- /dev/null +++ b/guppylang/diagnostic.py @@ -0,0 +1,111 @@ +from dataclasses import dataclass, field +from enum import Enum, auto +from typing import ClassVar, Literal, Protocol, runtime_checkable + +from typing_extensions import Self + +from guppylang.error import InternalGuppyError +from guppylang.span import ToSpan, to_span + + +class DiagnosticLevel(Enum): + """Severity levels for compiler diagnostics.""" + + #: An error that makes it impossible to proceed, causing an immediate abort. + FATAL = auto() + + #: A regular error that is encountered during compilation. This is the most common + #: diagnostic case. + ERROR = auto() + + #: A warning about the code being compiled. Doesn't prevent compilation from + #: finishing. + WARNING = auto() + + #: A message giving some additional context. Usually used as a sub-diagnostic of + #: errors. + NOTE = auto() + + #: A message suggesting how to fix something. Usually used as a sub-diagnostic of + #: errors. + HELP = auto() + + +@runtime_checkable +@dataclass(frozen=True) +class Diagnostic(Protocol): + """Abstract base class for compiler diagnostics that are reported to users. + + These could be fatal errors, regular errors, or warnings (see `DiagnosticLevel`). + """ + + #: Severity level of the diagnostic. + level: ClassVar[DiagnosticLevel] + + #: Primary span of the source location associated with this diagnostic. The span + #: is optional, but provided in almost all cases. + span: ToSpan | None + + #: Short title for the diagnostic that is displayed at the top. + title: ClassVar[str] + + #: Longer message that is printed below the span. + long_message: ClassVar[str | None] = None + + #: Label that is printed next to the span highlight. Can only be used if a span is + #: provided. + span_label: ClassVar[str | None] = None + + #: Optional sub-diagnostics giving some additional context. + children: list["SubDiagnostic"] = field(default_factory=list, init=False) + + def __post_init__(self) -> None: + if self.span_label and not self.span: + raise InternalGuppyError("Diagnostic: Span label provided without span") + + def add_sub_diagnostic(self, sub: "SubDiagnostic") -> Self: + """Adds a new sub-diagnostic.""" + if self.span and sub.span and to_span(sub.span).file != to_span(self.span).file: + raise InternalGuppyError( + "Diagnostic: Cross-file sub-diagnostics are not supported" + ) + self.children.append(sub) + return self + + +@runtime_checkable +@dataclass(frozen=True) +class SubDiagnostic(Protocol): + """A sub-diagnostic attached to a parent diagnostic. + + Can be used to give some additional context, for example a note attached to an + error. + """ + + #: Severity level of the sub-diagnostic. + level: ClassVar[DiagnosticLevel] + + #: Optional span of the source location associated with this sub-diagnostic. + span: ToSpan | None + + #: Label that is printed next to the span highlight. Can only be used if a span is + #: provided. + span_label: ClassVar[str | None] = None + + #: Message that is printed if no span is provided. + message: ClassVar[str | None] = None + + def __post_init__(self) -> None: + if self.span_label and not self.span: + raise InternalGuppyError("SubDiagnostic: Span label provided without span") + if not self.span and not self.message: + raise InternalGuppyError("SubDiagnostic: Empty diagnostic") + + +@runtime_checkable +@dataclass(frozen=True) +class Error(Diagnostic, Protocol): + """Compiler diagnostic for regular errors that are encountered during + compilation.""" + + level: ClassVar[Literal[DiagnosticLevel.ERROR]] = DiagnosticLevel.ERROR diff --git a/guppylang/span.py b/guppylang/span.py new file mode 100644 index 00000000..e92d9688 --- /dev/null +++ b/guppylang/span.py @@ -0,0 +1,82 @@ +"""Source spans representing locations in the code being compiled.""" + +import ast +from dataclasses import dataclass +from typing import TypeAlias + +from guppylang.ast_util import get_file, get_line_offset +from guppylang.error import InternalGuppyError + + +@dataclass(frozen=True, order=True) +class Loc: + """A location in a source file.""" + + file: str + + #: Line number starting at 1 + line: int + + #: Column number starting at 1 + column: int + + +@dataclass(frozen=True) +class Span: + """A continuous sequence of source code within a file.""" + + #: Starting location of the span (inclusive) + start: Loc + + # Ending location of the span (exclusive) + end: Loc + + def __post_init__(self) -> None: + if self.start.file != self.end.file: + raise InternalGuppyError("Span: Source spans multiple files") + if self.start > self.end: + raise InternalGuppyError("Span: Start after end") + + def __contains__(self, x: "Span | Loc") -> bool: + """Determines whether another span or location is completely contained in this + span.""" + if self.file != x.file: + return False + if isinstance(x, Span): + return self.start <= x.start <= self.end <= x.end + return self.start <= x <= self.end + + def __and__(self, other: "Span") -> "Span | None": + """Returns the intersection with the given span or `None` if they don't + intersect.""" + if self.file != other.file: + return None + if self.start > other.end or other.start > self.end: + return None + return Span(max(self.start, other.start), min(self.end, other.end)) + + @property + def file(self) -> str: + """The file containing this span.""" + return self.start.file + + +#: Objects in the compiler that are associated with a source span +ToSpan: TypeAlias = ast.AST | Span + + +def to_span(x: ToSpan) -> Span: + """Extracts a source span from an object.""" + if isinstance(x, Span): + return x + file, line_offset = get_file(x), get_line_offset(x) + assert file is not None + assert line_offset is not None + # x.lineno and line_offset both start at 1, so we have to subtract 1 + start = Loc(file, x.lineno + line_offset - 1, x.col_offset) + end = Loc( + file, + (x.end_lineno or x.lineno) + line_offset - 1, + x.end_col_offset or x.col_offset, + ) + return Span(start, end) From 7f07cc7407dbc3399c82cd28ea396054bf87a47e Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:07:23 +0100 Subject: [PATCH 03/20] feat: Cache accessed source files (#551) We used to store the entire source code that was used to build an AST as a field within *every node* of the AST. This was a hack so that we can render the source line from an AST node when an error occurs. With our new diagnostics system, we should do better than that! With this PR, we cache the source of every file accessed by the compiler in a new `SourceMap` class. Then, we can look up the source associated with a span when rendering a diagnostic. Note that we can't rely on Pythons `linecache` since we also need to keep track of cells in jupyter notebooks as individual "files". --- guppylang/decorator.py | 18 ++++++++++++++---- guppylang/definition/common.py | 4 +++- guppylang/definition/const.py | 3 ++- guppylang/definition/custom.py | 3 ++- guppylang/definition/declaration.py | 5 +++-- guppylang/definition/extern.py | 3 ++- guppylang/definition/function.py | 13 ++++++++----- guppylang/definition/struct.py | 9 ++++++--- guppylang/module.py | 15 ++++++++++++--- guppylang/span.py | 29 +++++++++++++++++++++++++++++ 10 files changed, 81 insertions(+), 21 deletions(-) diff --git a/guppylang/decorator.py b/guppylang/decorator.py index 645c5f06..90d79e9b 100644 --- a/guppylang/decorator.py +++ b/guppylang/decorator.py @@ -38,6 +38,7 @@ find_guppy_module_in_py_module, get_calling_frame, ) +from guppylang.span import SourceMap from guppylang.tys.subst import Inst from guppylang.tys.ty import NumericType @@ -77,8 +78,12 @@ class _Guppy: # The currently-alive GuppyModules, associated with a Python file/module _modules: dict[ModuleIdentifier, GuppyModule] + # Storage for source code that has been read by the compiler + _sources: SourceMap + def __init__(self) -> None: self._modules = {} + self._sources = SourceMap() @overload def __call__(self, arg: PyFunc) -> RawFunctionDef: ... @@ -300,7 +305,7 @@ def custom( mod = module or self.get_module() def dec(f: PyFunc) -> RawCustomFunctionDef: - func_ast, docstring = parse_py_func(f) + func_ast, docstring = parse_py_func(f, self._sources) if not has_empty_body(func_ast): raise GuppyError( "Body of custom function declaration must be empty", @@ -360,7 +365,9 @@ def constant( ) -> RawConstDef: """Adds a constant to a module, backed by a `hugr.val.Value`.""" module = module or self.get_module() - type_ast = _parse_expr_string(ty, f"Not a valid Guppy type: `{ty}`") + type_ast = _parse_expr_string( + ty, f"Not a valid Guppy type: `{ty}`", self._sources + ) defn = RawConstDef(DefId.fresh(module), name, None, type_ast, value) module.register_def(defn) return defn @@ -375,7 +382,9 @@ def extern( ) -> RawExternDef: """Adds an extern symbol to a module.""" module = module or self.get_module() - type_ast = _parse_expr_string(ty, f"Not a valid Guppy type: `{ty}`") + type_ast = _parse_expr_string( + ty, f"Not a valid Guppy type: `{ty}`", self._sources + ) defn = RawExternDef( DefId.fresh(module), name, None, symbol or name, constant, type_ast ) @@ -444,7 +453,7 @@ def registered_modules(self) -> KeysView[ModuleIdentifier]: guppy = _Guppy() -def _parse_expr_string(ty_str: str, parse_err: str) -> ast.expr: +def _parse_expr_string(ty_str: str, parse_err: str, sources: SourceMap) -> ast.expr: """Helper function to parse expressions that are provided as strings. Tries to infer the source location were the given string was defined by inspecting @@ -460,6 +469,7 @@ def _parse_expr_string(ty_str: str, parse_err: str) -> ast.expr: if caller_frame := get_calling_frame(): info = inspect.getframeinfo(caller_frame) if caller_module := inspect.getmodule(caller_frame): + sources.add_file(info.filename) source_lines, _ = inspect.getsourcelines(caller_module) source = "".join(source_lines) annotate_location(expr_ast, source, info.filename, 0) diff --git a/guppylang/definition/common.py b/guppylang/definition/common.py index 73e8278b..0a11d882 100644 --- a/guppylang/definition/common.py +++ b/guppylang/definition/common.py @@ -8,6 +8,8 @@ from hugr.build.dfg import DefinitionBuilder, OpVar from hugr.ext import Package +from guppylang.span import SourceMap + if TYPE_CHECKING: from guppylang.checker.core import Globals from guppylang.compiler.core import CompiledGlobals @@ -92,7 +94,7 @@ class ParsableDef(Definition): """ @abstractmethod - def parse(self, globals: "Globals") -> ParsedDef: + def parse(self, globals: "Globals", sources: SourceMap) -> ParsedDef: """Performs parsing and validation, returning a definition that can be checked. The provided globals contain all other raw definitions that have been defined. diff --git a/guppylang/definition/const.py b/guppylang/definition/const.py index d7479425..1f691317 100644 --- a/guppylang/definition/const.py +++ b/guppylang/definition/const.py @@ -10,6 +10,7 @@ from guppylang.compiler.core import CompiledGlobals, DFContainer from guppylang.definition.common import CompilableDef, ParsableDef from guppylang.definition.value import CompiledValueDef, ValueDef +from guppylang.span import SourceMap from guppylang.tys.parsing import type_from_ast @@ -22,7 +23,7 @@ class RawConstDef(ParsableDef): description: str = field(default="constant", init=False) - def parse(self, globals: Globals) -> "ConstDef": + def parse(self, globals: Globals, sources: SourceMap) -> "ConstDef": """Parses and checks the user-provided signature of the function.""" return ConstDef( self.id, diff --git a/guppylang/definition/custom.py b/guppylang/definition/custom.py index a3eeef5a..ee250d7b 100644 --- a/guppylang/definition/custom.py +++ b/guppylang/definition/custom.py @@ -16,6 +16,7 @@ from guppylang.definition.value import CallReturnWires, CompiledCallableDef from guppylang.error import GuppyError, InternalGuppyError from guppylang.nodes import GlobalCall +from guppylang.span import SourceMap from guppylang.tys.subst import Inst, Subst from guppylang.tys.ty import ( FuncInput, @@ -56,7 +57,7 @@ class RawCustomFunctionDef(ParsableDef): description: str = field(default="function", init=False) - def parse(self, globals: "Globals") -> "CustomFunctionDef": + def parse(self, globals: "Globals", sources: SourceMap) -> "CustomFunctionDef": """Parses and checks the user-provided signature of the custom function. The signature is optional if custom type checking logic is provided by the user. diff --git a/guppylang/definition/declaration.py b/guppylang/definition/declaration.py index 2352ca7b..7a77ac10 100644 --- a/guppylang/definition/declaration.py +++ b/guppylang/definition/declaration.py @@ -16,6 +16,7 @@ from guppylang.definition.value import CallableDef, CallReturnWires, CompiledCallableDef from guppylang.error import GuppyError from guppylang.nodes import GlobalCall +from guppylang.span import SourceMap from guppylang.tys.subst import Inst, Subst from guppylang.tys.ty import Type, type_to_row @@ -32,9 +33,9 @@ class RawFunctionDecl(ParsableDef): python_scope: PyScope description: str = field(default="function", init=False) - def parse(self, globals: Globals) -> "CheckedFunctionDecl": + def parse(self, globals: Globals, sources: SourceMap) -> "CheckedFunctionDecl": """Parses and checks the user-provided signature of the function.""" - func_ast, docstring = parse_py_func(self.python_func) + func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature(func_ast, globals.with_python_scope(self.python_scope)) if not has_empty_body(func_ast): raise GuppyError( diff --git a/guppylang/definition/extern.py b/guppylang/definition/extern.py index 3950750d..a664c91a 100644 --- a/guppylang/definition/extern.py +++ b/guppylang/definition/extern.py @@ -9,6 +9,7 @@ from guppylang.compiler.core import CompiledGlobals, DFContainer from guppylang.definition.common import CompilableDef, ParsableDef from guppylang.definition.value import CompiledValueDef, ValueDef +from guppylang.span import SourceMap from guppylang.tys.parsing import type_from_ast @@ -22,7 +23,7 @@ class RawExternDef(ParsableDef): description: str = field(default="extern", init=False) - def parse(self, globals: Globals) -> "ExternDef": + def parse(self, globals: Globals, sources: SourceMap) -> "ExternDef": """Parses and checks the user-provided signature of the function.""" return ExternDef( self.id, diff --git a/guppylang/definition/function.py b/guppylang/definition/function.py index 3920fe32..8187566f 100644 --- a/guppylang/definition/function.py +++ b/guppylang/definition/function.py @@ -26,6 +26,7 @@ from guppylang.error import GuppyError from guppylang.ipython_inspect import find_ipython_def, is_running_ipython from guppylang.nodes import GlobalCall +from guppylang.span import SourceMap from guppylang.tys.subst import Inst, Subst from guppylang.tys.ty import FunctionType, Type, type_to_row @@ -53,9 +54,9 @@ class RawFunctionDef(ParsableDef): description: str = field(default="function", init=False) - def parse(self, globals: Globals) -> "ParsedFunctionDef": + def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": """Parses and checks the user-provided signature of the function.""" - func_ast, docstring = parse_py_func(self.python_func) + func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature(func_ast, globals.with_python_scope(self.python_scope)) if ty.parametrized: raise GuppyError( @@ -220,7 +221,7 @@ def compile_inner(self, globals: CompiledGlobals) -> None: compile_global_func_def(self, self.func_def, globals) -def parse_py_func(f: PyFunc) -> tuple[ast.FunctionDef, str | None]: +def parse_py_func(f: PyFunc, sources: SourceMap) -> tuple[ast.FunctionDef, str | None]: source_lines, line_offset = inspect.getsourcelines(f) source = "".join(source_lines) # Lines already have trailing \n's source = textwrap.dedent(source) @@ -234,10 +235,12 @@ def parse_py_func(f: PyFunc) -> tuple[ast.FunctionDef, str | None]: defn = find_ipython_def(func_ast.name) if defn is not None: file = f"<{defn.cell_name}>" + sources.add_file(file, source) else: file = inspect.getsourcefile(f) - if file is None: - raise GuppyError("Couldn't determine source file for function") + if file is None: + raise GuppyError("Couldn't determine source file for function") + sources.add_file(file) annotate_location(func_ast, source, file, line_offset) if not isinstance(func_ast, ast.FunctionDef): raise GuppyError("Expected a function definition", func_ast) diff --git a/guppylang/definition/struct.py b/guppylang/definition/struct.py index 521b5549..f1573322 100644 --- a/guppylang/definition/struct.py +++ b/guppylang/definition/struct.py @@ -25,6 +25,7 @@ from guppylang.definition.ty import TypeDef from guppylang.error import GuppyError, InternalGuppyError from guppylang.ipython_inspect import find_ipython_def, is_running_ipython +from guppylang.span import SourceMap from guppylang.tys.arg import Argument from guppylang.tys.param import Parameter, check_all_args from guppylang.tys.parsing import type_from_ast @@ -63,9 +64,9 @@ def __getitem__(self, item: Any) -> "RawStructDef": """ return self - def parse(self, globals: Globals) -> "ParsedStructDef": + def parse(self, globals: Globals, sources: SourceMap) -> "ParsedStructDef": """Parses the raw class object into an AST and checks that it is well-formed.""" - cls_def = parse_py_class(self.python_class) + cls_def = parse_py_class(self.python_class, sources) if cls_def.keywords: raise GuppyError("Unexpected keyword", cls_def.keywords[0]) @@ -232,7 +233,7 @@ def compile(self, args: list[Wire]) -> list[Wire]: return [constructor_def] -def parse_py_class(cls: type) -> ast.ClassDef: +def parse_py_class(cls: type, sources: SourceMap) -> ast.ClassDef: """Parses a Python class object into an AST.""" # If we are running IPython, `inspect.getsourcelines` works only for builtins # (guppy stdlib), but not for most/user-defined classes - see: @@ -254,6 +255,8 @@ def parse_py_class(cls: type) -> ast.ClassDef: file = inspect.getsourcefile(cls) if file is None: raise GuppyError("Couldn't determine source file for class") + # Store the source file in our cache + sources.add_file(file) annotate_location(cls_ast, source, file, line_offset) if not isinstance(cls_ast, ast.ClassDef): raise GuppyError("Expected a class definition", cls_ast) diff --git a/guppylang/module.py b/guppylang/module.py index c4a2ced4..4befecd2 100644 --- a/guppylang/module.py +++ b/guppylang/module.py @@ -29,6 +29,7 @@ from guppylang.definition.ty import TypeDef from guppylang.error import GuppyError, pretty_errors from guppylang.experimental import enable_experimental_features +from guppylang.span import SourceMap PyClass = type PyFunc = Callable[..., Any] @@ -73,6 +74,9 @@ class GuppyModule: # `_register_buffered_instance_funcs` is called. This way, we can associate _instance_func_buffer: dict[str, RawDef] | None + # Storage for source code that has been read by the compiler + _sources: SourceMap + def __init__(self, name: str, import_builtins: bool = True): self.name = name self._globals = Globals({}, {}, {}, {}) @@ -86,6 +90,10 @@ def __init__(self, name: str, import_builtins: bool = True): self._raw_type_defs = {} self._checked_defs = {} + from guppylang.decorator import guppy + + self._sources = guppy._sources + # Import builtin module if import_builtins: import guppylang.prelude.builtins as builtins @@ -249,14 +257,15 @@ def checked(self) -> bool: def compiled(self) -> bool: return self._compiled - @staticmethod def _check_defs( - raw_defs: Mapping[DefId, RawDef], globals: Globals + self, raw_defs: Mapping[DefId, RawDef], globals: Globals ) -> dict[DefId, CheckedDef]: """Helper method to parse and check raw definitions.""" raw_globals = globals | Globals(dict(raw_defs), {}, {}, {}) parsed = { - def_id: defn.parse(raw_globals) if isinstance(defn, ParsableDef) else defn + def_id: defn.parse(raw_globals, self._sources) + if isinstance(defn, ParsableDef) + else defn for def_id, defn in raw_defs.items() } parsed_globals = globals | Globals(dict(parsed), {}, {}, {}) diff --git a/guppylang/span.py b/guppylang/span.py index e92d9688..743e289a 100644 --- a/guppylang/span.py +++ b/guppylang/span.py @@ -1,6 +1,7 @@ """Source spans representing locations in the code being compiled.""" import ast +import linecache from dataclasses import dataclass from typing import TypeAlias @@ -80,3 +81,31 @@ def to_span(x: ToSpan) -> Span: x.end_col_offset or x.col_offset, ) return Span(start, end) + + +#: List of source lines in a file +SourceLines: TypeAlias = list[str] + + +class SourceMap: + """Map holding the source code for all files accessed by the compiler. + + Can be used to look up the source code associated with a span. + """ + + sources: dict[str, SourceLines] + + def __init__(self) -> None: + self.sources = {} + + def add_file(self, file: str, content: str | None = None) -> None: + """Registers a new source file.""" + if content is None: + self.sources[file] = [line.rstrip() for line in linecache.getlines(file)] + else: + self.sources[file] = content.splitlines(keepends=False) + + def span_lines(self, span: Span, prefix_lines: int = 0) -> list[str]: + return self.sources[span.file][ + span.start.line - prefix_lines - 1 : span.end.line + ] From f9709aeacb70292ffb8b8a9ec61b7f4264735b38 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:13:12 +0100 Subject: [PATCH 04/20] feat: Render diagnostics (#552) Closes #537 --- guppylang/diagnostic.py | 306 +++++++++++++++--- guppylang/span.py | 37 +++ tests/diagnostics/__init__.py | 0 tests/diagnostics/snapshots/test_context.txt | 6 + tests/diagnostics/snapshots/test_help.txt | 6 + tests/diagnostics/snapshots/test_indented.txt | 6 + .../snapshots/test_justify_line_number.txt | 6 + .../diagnostics/snapshots/test_long_label.txt | 15 + .../snapshots/test_message_with_span.txt | 7 + tests/diagnostics/snapshots/test_note.txt | 6 + .../diagnostics/snapshots/test_only_label.txt | 4 + .../snapshots/test_only_message.txt | 2 + .../diagnostics/snapshots/test_only_title.txt | 1 + .../snapshots/test_three_labels_formatted.txt | 10 + .../snapshots/test_three_line_span.txt | 7 + .../snapshots/test_two_line_span.txt | 6 + .../diagnostics/test_diagnostics_rendering.py | 169 ++++++++++ 17 files changed, 552 insertions(+), 42 deletions(-) create mode 100644 tests/diagnostics/__init__.py create mode 100644 tests/diagnostics/snapshots/test_context.txt create mode 100644 tests/diagnostics/snapshots/test_help.txt create mode 100644 tests/diagnostics/snapshots/test_indented.txt create mode 100644 tests/diagnostics/snapshots/test_justify_line_number.txt create mode 100644 tests/diagnostics/snapshots/test_long_label.txt create mode 100644 tests/diagnostics/snapshots/test_message_with_span.txt create mode 100644 tests/diagnostics/snapshots/test_note.txt create mode 100644 tests/diagnostics/snapshots/test_only_label.txt create mode 100644 tests/diagnostics/snapshots/test_only_message.txt create mode 100644 tests/diagnostics/snapshots/test_only_title.txt create mode 100644 tests/diagnostics/snapshots/test_three_labels_formatted.txt create mode 100644 tests/diagnostics/snapshots/test_three_line_span.txt create mode 100644 tests/diagnostics/snapshots/test_two_line_span.txt create mode 100644 tests/diagnostics/test_diagnostics_rendering.py diff --git a/guppylang/diagnostic.py b/guppylang/diagnostic.py index 92f75585..aeac49b2 100644 --- a/guppylang/diagnostic.py +++ b/guppylang/diagnostic.py @@ -1,11 +1,12 @@ -from dataclasses import dataclass, field +import textwrap +from dataclasses import dataclass, field, fields from enum import Enum, auto -from typing import ClassVar, Literal, Protocol, runtime_checkable +from typing import ClassVar, Final, Literal, Protocol, overload, runtime_checkable from typing_extensions import Self from guppylang.error import InternalGuppyError -from guppylang.span import ToSpan, to_span +from guppylang.span import Loc, SourceMap, Span, ToSpan, to_span class DiagnosticLevel(Enum): @@ -33,35 +34,79 @@ class DiagnosticLevel(Enum): @runtime_checkable @dataclass(frozen=True) -class Diagnostic(Protocol): - """Abstract base class for compiler diagnostics that are reported to users. +class SubDiagnostic(Protocol): + """A sub-diagnostic attached to a parent diagnostic. - These could be fatal errors, regular errors, or warnings (see `DiagnosticLevel`). + Can be used to give some additional context, for example a note attached to an + error. """ - #: Severity level of the diagnostic. + #: Severity level of the sub-diagnostic. level: ClassVar[DiagnosticLevel] - #: Primary span of the source location associated with this diagnostic. The span - #: is optional, but provided in almost all cases. + #: Optional span of the source location associated with this sub-diagnostic. span: ToSpan | None - #: Short title for the diagnostic that is displayed at the top. - title: ClassVar[str] - - #: Longer message that is printed below the span. - long_message: ClassVar[str | None] = None - #: Label that is printed next to the span highlight. Can only be used if a span is #: provided. span_label: ClassVar[str | None] = None + #: Message that is printed if no span is provided. + message: ClassVar[str | None] = None + + def __post_init__(self) -> None: + if self.span_label and self.span is None: + raise InternalGuppyError("SubDiagnostic: Span label provided without span") + + @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) + + @overload + def _render(self, s: str) -> str: ... + + @overload + def _render(self, s: None) -> None: ... + + def _render(self, s: str | None) -> str | None: + """Helper method to fill in placeholder values in strings with fields of this + diagnostic. + """ + values = {f.name: getattr(self, f.name) for f in fields(self)} + return s.format(**values) if s is not None else None + + +@runtime_checkable +@dataclass(frozen=True) +class Diagnostic(SubDiagnostic, Protocol): + """Abstract base class for compiler diagnostics that are reported to users. + + These could be fatal errors, regular errors, or warnings (see `DiagnosticLevel`). + """ + + #: Short title for the diagnostic that is displayed at the top. + title: ClassVar[str] + #: Optional sub-diagnostics giving some additional context. children: list["SubDiagnostic"] = field(default_factory=list, init=False) def __post_init__(self) -> None: - if self.span_label and not self.span: - raise InternalGuppyError("Diagnostic: Span label provided without span") + super().__post_init__() + if self.span is None and self.children: + raise InternalGuppyError( + "Diagnostic: Span-less diagnostics can't have children (FIXME)" + ) + + @property + def rendered_title(self) -> str: + """The title of this diagnostic with formatted placeholders.""" + return self._render(self.title) def add_sub_diagnostic(self, sub: "SubDiagnostic") -> Self: """Adds a new sub-diagnostic.""" @@ -75,37 +120,214 @@ def add_sub_diagnostic(self, sub: "SubDiagnostic") -> Self: @runtime_checkable @dataclass(frozen=True) -class SubDiagnostic(Protocol): - """A sub-diagnostic attached to a parent diagnostic. - - Can be used to give some additional context, for example a note attached to an - error. - """ - - #: Severity level of the sub-diagnostic. - level: ClassVar[DiagnosticLevel] +class Error(Diagnostic, Protocol): + """Compiler diagnostic for regular errors that are encountered during + compilation.""" - #: Optional span of the source location associated with this sub-diagnostic. - span: ToSpan | None + level: ClassVar[Literal[DiagnosticLevel.ERROR]] = DiagnosticLevel.ERROR - #: Label that is printed next to the span highlight. Can only be used if a span is - #: provided. - span_label: ClassVar[str | None] = None - #: Message that is printed if no span is provided. - message: ClassVar[str | None] = None +@runtime_checkable +@dataclass(frozen=True) +class Note(SubDiagnostic, Protocol): + """Compiler sub-diagnostic giving some additional context.""" - def __post_init__(self) -> None: - if self.span_label and not self.span: - raise InternalGuppyError("SubDiagnostic: Span label provided without span") - if not self.span and not self.message: - raise InternalGuppyError("SubDiagnostic: Empty diagnostic") + level: ClassVar[Literal[DiagnosticLevel.NOTE]] = DiagnosticLevel.NOTE @runtime_checkable @dataclass(frozen=True) -class Error(Diagnostic, Protocol): - """Compiler diagnostic for regular errors that are encountered during - compilation.""" +class Help(SubDiagnostic, Protocol): + """Compiler sub-diagnostic suggesting how to fix something.""" - level: ClassVar[Literal[DiagnosticLevel.ERROR]] = DiagnosticLevel.ERROR + level: ClassVar[Literal[DiagnosticLevel.HELP]] = DiagnosticLevel.HELP + + +class DiagnosticsRenderer: + """Standard renderer for compiler diagnostics in human-readable format.""" + + source: SourceMap + buffer: list[str] + + #: Maximum amount of leading whitespace until we start trimming it + MAX_LEADING_WHITESPACE: Final[int] = 12 + + #: Amount of leading whitespace left after trimming for padding + OPTIMAL_LEADING_WHITESPACE: Final[int] = 4 + + #: Maximum length of span labels after which we insert a newline + MAX_LABEL_LINE_LEN: Final[int] = 60 + + #: Maximum length of messages after which we insert a newline + MAX_MESSAGE_LINE_LEN: Final[int] = 80 + + #: Number of preceding source lines we show to give additional context + PREFIX_CONTEXT_LINES: Final[int] = 2 + + def __init__(self, source: SourceMap) -> None: + self.buffer = [] + self.source = source + + def render_diagnostic(self, diag: Diagnostic) -> None: + """Renders a single diagnostic together with its sub-diagnostics. + + Example: + + ``` + Error: Short title for the diagnostic (at path/to/file.py:line:column) + | + 42 | def foo(x: blah) -> None: + | ^^^^ Span label + | + 55 | x = bar() + baz + | ----- Sub-diagnostic label + + Longer message describing the error. + + note: Sub-diagnostic message without span + ``` + """ + if diag.span is None: + # Omit the title if we don't have a span, but a long message. This case + # should be fairly rare. + msg = diag.rendered_message or diag.rendered_title + self.buffer += textwrap.wrap( + f"{self.level_str(diag.level)}: {msg}", + self.MAX_MESSAGE_LINE_LEN, + ) + else: + span = to_span(diag.span) + level = self.level_str(diag.level) + self.buffer.append(f"{level}: {diag.rendered_title} (at {span.start})") + self.render_snippet( + span, + diag.rendered_span_label, + is_primary=True, + prefix_lines=self.PREFIX_CONTEXT_LINES, + ) + # First render all sub-diagnostics that come with a span + for sub_diag in diag.children: + if sub_diag.span: + self.render_snippet( + to_span(sub_diag.span), + sub_diag.rendered_span_label, + is_primary=False, + ) + if diag.message: + self.buffer.append("") + self.buffer += textwrap.wrap( + f"{diag.rendered_message}", self.MAX_MESSAGE_LINE_LEN + ) + # Finally, render all sub-diagnostics that have a non-span message + for sub_diag in diag.children: + if sub_diag.message: + self.buffer.append("") + self.buffer += textwrap.wrap( + f"{self.level_str(sub_diag.level)}: {sub_diag.rendered_message}", + self.MAX_MESSAGE_LINE_LEN, + ) + + def render_snippet( + self, span: Span, label: str | None, is_primary: bool, prefix_lines: int = 0 + ) -> None: + """Renders the source associated with a span together with an optional label. + + ``` + | + 42 | def foo(x: blah) -> None: + | ^^^^ Span label. This could cover + | multiple lines! + ``` + + Also supports spans covering multiple lines: + + ``` + | + 42 | def foo(x: int) -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | ... + 48 | return bar() + | ^^^^^^^^^^^^^^^^ Label covering the entire definition of foo + ``` + + If `is_primary` is `False`, the span is highlighted using `-` instead of `^`: + + ``` + | + 42 | def foo(x: blah) -> None: + | ---- Non-primary span label + ``` + + Optionally includes up to `prefix_lines` preceding source lines to give + additional context. + """ + # Check how much space we need to reserve for the leading line numbers + ll_length = len(str(span.end.line)) + highlight_char = "^" if is_primary else "-" + + def render_line(line: str, line_number: int | None = None) -> None: + """Helper method to render a line with the line number bar on the left.""" + ll = "" if line_number is None else str(line_number) + self.buffer.append(" " * (ll_length - len(ll)) + ll + " | " + line) + + # One line of padding + render_line("") + + # Grab all lines we want to display and remove excessive leading whitespace + prefix_lines = min(prefix_lines, span.start.line - 1) + all_lines = self.source.span_lines(span, prefix_lines) + leading_whitespace = min(len(line) - len(line.lstrip()) for line in all_lines) + if leading_whitespace > self.MAX_LEADING_WHITESPACE: + remove = leading_whitespace - self.OPTIMAL_LEADING_WHITESPACE + all_lines = [line[remove:] for line in all_lines] + span = span.shift_left(remove) + + # Render prefix lines + for i, line in enumerate(all_lines[:prefix_lines]): + render_line(line, span.start.line - prefix_lines + i) + span_lines = all_lines[prefix_lines:] + + if span.is_multiline: + [first, *middle, last] = span_lines + render_line(first, span.start.line) + # Compute the subspan that only covers the first line and render its + # highlight banner + first_span = Span(span.start, Loc(span.file, span.start.line, len(first))) + first_highlight = " " * first_span.start.column + highlight_char * len( + first_span + ) + render_line(first_highlight) + # Omit everything in the middle + if middle: + render_line("...") + # The last line is handled uniformly with the single-line case below. + # Therefore, create a subspan that only covers the last line. + last_span = Span(Loc(span.file, span.end.line, 0), span.end) + else: + [last] = span_lines + last_span = span + + # Render the last span line and add highlights + render_line(last, span.end.line) + last_highlight = " " * last_span.start.column + highlight_char * len(last_span) + + # Render the label next to the highlight + if label: + [label_first, *label_rest] = textwrap.wrap( + label, + self.MAX_LABEL_LINE_LEN, + # One space after the last `^` + initial_indent=" ", + # Indent all subsequent lines to be aligned + subsequent_indent=" " * (len(last_highlight) + 1), + ) + render_line(last_highlight + label_first) + for lbl in label_rest: + render_line(lbl) + else: + render_line(last_highlight) + + @staticmethod + def level_str(level: DiagnosticLevel) -> str: + """Returns the text used to identify the different kinds of diagnostics.""" + return level.name.lower().capitalize() diff --git a/guppylang/span.py b/guppylang/span.py index 743e289a..d5492a51 100644 --- a/guppylang/span.py +++ b/guppylang/span.py @@ -21,6 +21,19 @@ class Loc: #: Column number starting at 1 column: int + def __str__(self) -> str: + """Returns the string representation of this source location.""" + return f"{self.file}:{self.line}:{self.column}" + + def shift_left(self, cols: int) -> "Loc": + """Returns a new location shifted to left by the given number of columns.""" + assert self.column >= cols + return Loc(self.file, self.line, self.column - cols) + + def shift_right(self, cols: int) -> "Loc": + """Returns a new location shifted to right by the given number of columns.""" + return Loc(self.file, self.line, self.column + cols) + @dataclass(frozen=True) class Span: @@ -56,11 +69,35 @@ def __and__(self, other: "Span") -> "Span | None": return None return Span(max(self.start, other.start), min(self.end, other.end)) + def __len__(self) -> int: + """Returns the length of a single-line span in columns. + + Querying the length of multiline spans raises an `InternalGuppyError`. + """ + if self.is_multiline: + raise InternalGuppyError("Span: Tried to compute length of multi-line span") + return self.end.column - self.start.column + @property def file(self) -> str: """The file containing this span.""" return self.start.file + @property + def is_multiline(self) -> bool: + """Whether this source sequence spans multiple lines.""" + return self.start.line != self.end.line + + def shift_left(self, cols: int) -> "Span": + """Returns a new span that is shifted to the left by the given number of + columns.""" + return Span(self.start.shift_left(cols), self.end.shift_left(cols)) + + def shift_right(self, cols: int) -> "Span": + """Returns a new span that is shifted to the right by the given number of + columns.""" + return Span(self.start.shift_right(cols), self.end.shift_right(cols)) + #: Objects in the compiler that are associated with a source span ToSpan: TypeAlias = ast.AST | Span diff --git a/tests/diagnostics/__init__.py b/tests/diagnostics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/diagnostics/snapshots/test_context.txt b/tests/diagnostics/snapshots/test_context.txt new file mode 100644 index 00000000..17fce75d --- /dev/null +++ b/tests/diagnostics/snapshots/test_context.txt @@ -0,0 +1,6 @@ +Error: Can't compare apples with oranges (at :3:6) + | +1 | super_apple := apple ** 2 +2 | lemon := orange - apple +3 | apple == orange + | ^^ Comparison attempted here \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_help.txt b/tests/diagnostics/snapshots/test_help.txt new file mode 100644 index 00000000..75640394 --- /dev/null +++ b/tests/diagnostics/snapshots/test_help.txt @@ -0,0 +1,6 @@ +Error: Can't compare apples with oranges (at :1:6) + | +1 | apple == orange + | ^^ Comparison attempted here + +Help: Have you tried peeling the orange? \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_indented.txt b/tests/diagnostics/snapshots/test_indented.txt new file mode 100644 index 00000000..651c8738 --- /dev/null +++ b/tests/diagnostics/snapshots/test_indented.txt @@ -0,0 +1,6 @@ +Error: Can't compare apples with oranges (at :3:64) + | +1 | super_apple := apple ** 2 +2 | lemon := orange - apple +3 | apple == orange + | ^^ Comparison attempted here \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_justify_line_number.txt b/tests/diagnostics/snapshots/test_justify_line_number.txt new file mode 100644 index 00000000..f8ec8fac --- /dev/null +++ b/tests/diagnostics/snapshots/test_justify_line_number.txt @@ -0,0 +1,6 @@ +Error: Can't compare apples with oranges (at :100:6) + | + 98 | foo + 99 | foo +100 | apple == orange + | ^^ Comparison attempted here \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_long_label.txt b/tests/diagnostics/snapshots/test_long_label.txt new file mode 100644 index 00000000..e9419bc4 --- /dev/null +++ b/tests/diagnostics/snapshots/test_long_label.txt @@ -0,0 +1,15 @@ +Error: Can't compare apples with oranges (at :1:6) + | +1 | apple == orange + | ^^ Comparison attempted here. Comparison attempted here. + | Comparison attempted here. Comparison attempted + | here. Comparison attempted here. Comparison + | attempted here. Comparison attempted here. + | Comparison attempted here. Comparison attempted + | here. Comparison attempted here. Comparison + | attempted here. Comparison attempted here. + | Comparison attempted here. Comparison attempted + | here. Comparison attempted here. Comparison + | attempted here. Comparison attempted here. + | Comparison attempted here. Comparison attempted + | here. Comparison attempted here. \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_message_with_span.txt b/tests/diagnostics/snapshots/test_message_with_span.txt new file mode 100644 index 00000000..089aad5d --- /dev/null +++ b/tests/diagnostics/snapshots/test_message_with_span.txt @@ -0,0 +1,7 @@ +Error: Can't compare apples with oranges (at :1:6) + | +1 | apple == orange + | ^^ + +Please refer to Barone (BMJ, 2000), https://doi.org/10.1136%2Fbmj.321.7276.1569 +for further details. \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_note.txt b/tests/diagnostics/snapshots/test_note.txt new file mode 100644 index 00000000..47b68400 --- /dev/null +++ b/tests/diagnostics/snapshots/test_note.txt @@ -0,0 +1,6 @@ +Error: Can't compare apples with oranges (at :1:6) + | +1 | apple == orange + | ^^ Comparison attempted here + +Note: Stop trying, this is a fruitless endeavor \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_only_label.txt b/tests/diagnostics/snapshots/test_only_label.txt new file mode 100644 index 00000000..2d19b9be --- /dev/null +++ b/tests/diagnostics/snapshots/test_only_label.txt @@ -0,0 +1,4 @@ +Error: Can't compare apples with oranges (at :1:6) + | +1 | apple == orange + | ^^ Comparison attempted here \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_only_message.txt b/tests/diagnostics/snapshots/test_only_message.txt new file mode 100644 index 00000000..63a2084e --- /dev/null +++ b/tests/diagnostics/snapshots/test_only_message.txt @@ -0,0 +1,2 @@ +Error: Can't compare apples with oranges. Please refer to Barone (BMJ, 2000), +https://doi.org/10.1136%2Fbmj.321.7276.1569 for further details. \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_only_title.txt b/tests/diagnostics/snapshots/test_only_title.txt new file mode 100644 index 00000000..1f9f5ca5 --- /dev/null +++ b/tests/diagnostics/snapshots/test_only_title.txt @@ -0,0 +1 @@ +Error: Can't compare apples with oranges \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_three_labels_formatted.txt b/tests/diagnostics/snapshots/test_three_labels_formatted.txt new file mode 100644 index 00000000..1970516e --- /dev/null +++ b/tests/diagnostics/snapshots/test_three_labels_formatted.txt @@ -0,0 +1,10 @@ +Error: Can't compare apples with oranges (at :1:6) + | +1 | apple == orange + | ^^ Comparison attempted here + | +1 | apple == orange + | ----- This is an apple + | +1 | apple == orange + | ------ This is an orange \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_three_line_span.txt b/tests/diagnostics/snapshots/test_three_line_span.txt new file mode 100644 index 00000000..005e4313 --- /dev/null +++ b/tests/diagnostics/snapshots/test_three_line_span.txt @@ -0,0 +1,7 @@ +Error: Can't compare apples with oranges (at :1:5) + | +1 | apple.compare( + | ^^^^^^^^^ + | ... +3 | ) == EQUAL + | ^ Comparison attempted here \ No newline at end of file diff --git a/tests/diagnostics/snapshots/test_two_line_span.txt b/tests/diagnostics/snapshots/test_two_line_span.txt new file mode 100644 index 00000000..8510304a --- /dev/null +++ b/tests/diagnostics/snapshots/test_two_line_span.txt @@ -0,0 +1,6 @@ +Error: Can't compare apples with oranges (at :1:5) + | +1 | apple.compare( + | ^^^^^^^^^ +2 | orange) == EQUAL + | ^^^^^^^^^^^^ Comparison attempted here \ No newline at end of file diff --git a/tests/diagnostics/test_diagnostics_rendering.py b/tests/diagnostics/test_diagnostics_rendering.py new file mode 100644 index 00000000..aaa7ff22 --- /dev/null +++ b/tests/diagnostics/test_diagnostics_rendering.py @@ -0,0 +1,169 @@ +"""Snapshot tests for diagnostics rendering""" + +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar + +from guppylang.diagnostic import ( + Diagnostic, + DiagnosticsRenderer, + Error, + Help, + Note, +) +from guppylang.span import Loc, SourceMap, Span + +file = "" + + +def run_test(source: str, diagnostic: Diagnostic, snapshot, request): + sources = SourceMap() + sources.add_file(file, source) + + renderer = DiagnosticsRenderer(sources) + renderer.render_diagnostic(diagnostic) + out = "\n".join(renderer.buffer) + + snapshot.snapshot_dir = str(Path(request.fspath).parent / "snapshots") + snapshot.assert_match(out, f"{request.node.name}.txt") + + +@dataclass(frozen=True) +class MyError(Error): + title: ClassVar[str] = "Can't compare apples with oranges" + span_label: ClassVar[str] = "Comparison attempted here" + + +def test_only_title(snapshot, request): + @dataclass(frozen=True) + class MyDiagnostic(Error): + title: ClassVar[str] = "Can't compare apples with oranges" + + source = "" + diagnostic = MyDiagnostic(None) + run_test(source, diagnostic, snapshot, request) + + +def test_only_message(snapshot, request): + @dataclass(frozen=True) + class MyDiagnostic(Error): + title: ClassVar[str] = "Can't compare apples with oranges" + message: ClassVar[str] = ( + "Can't compare apples with oranges. Please refer to Barone (BMJ, 2000), " + "https://doi.org/10.1136%2Fbmj.321.7276.1569 for further details." + ) + + source = "" + diagnostic = MyDiagnostic(None) + run_test(source, diagnostic, snapshot, request) + + +def test_only_label(snapshot, request): + source = "apple == orange" + span = Span(Loc(file, 1, 6), Loc(file, 1, 8)) + diagnostic = MyError(span) + run_test(source, diagnostic, snapshot, request) + + +def test_message_with_span(snapshot, request): + @dataclass(frozen=True) + class MyDiagnostic(Error): + title: ClassVar[str] = "Can't compare apples with oranges" + message: ClassVar[str] = ( + "Please refer to Barone (BMJ, 2000), " + "https://doi.org/10.1136%2Fbmj.321.7276.1569 for further details." + ) + + source = "apple == orange" + span = Span(Loc(file, 1, 6), Loc(file, 1, 8)) + diagnostic = MyDiagnostic(span) + run_test(source, diagnostic, snapshot, request) + + +def test_three_labels_formatted(snapshot, request): + @dataclass(frozen=True) + class MySubDiagnostic(Note): + thing: str + span_label: ClassVar[str] = "This is an {thing}" + + source = "apple == orange" + span = Span(Loc(file, 1, 6), Loc(file, 1, 8)) + span_apple = Span(Loc(file, 1, 0), Loc(file, 1, 5)) + span_orange = Span(Loc(file, 1, 9), Loc(file, 1, 15)) + diagnostic = MyError(span) + diagnostic.add_sub_diagnostic(MySubDiagnostic(span_apple, "apple")) + diagnostic.add_sub_diagnostic(MySubDiagnostic(span_orange, "orange")) + run_test(source, diagnostic, snapshot, request) + + +def test_long_label(snapshot, request): + @dataclass(frozen=True) + class MyDiagnostic(Error): + title: ClassVar[str] = "Can't compare apples with oranges" + span_label: ClassVar[str] = "Comparison attempted here. " * 20 + + source = "apple == orange" + span = Span(Loc(file, 1, 6), Loc(file, 1, 8)) + diagnostic = MyDiagnostic(span) + run_test(source, diagnostic, snapshot, request) + + +def test_help(snapshot, request): + @dataclass(frozen=True) + class MySubDiagnostic(Help): + message: ClassVar[str] = "Have you tried peeling the orange?" + + source = "apple == orange" + span = Span(Loc(file, 1, 6), Loc(file, 1, 8)) + diagnostic = MyError(span) + diagnostic.add_sub_diagnostic(MySubDiagnostic(None)) + run_test(source, diagnostic, snapshot, request) + + +def test_note(snapshot, request): + @dataclass(frozen=True) + class MySubDiagnostic(Note): + message: ClassVar[str] = "Stop trying, this is a fruitless endeavor" + + source = "apple == orange" + span = Span(Loc(file, 1, 6), Loc(file, 1, 8)) + diagnostic = MyError(span) + diagnostic.add_sub_diagnostic(MySubDiagnostic(None)) + run_test(source, diagnostic, snapshot, request) + + +def test_context(snapshot, request): + source = "super_apple := apple ** 2\nlemon := orange - apple\napple == orange" + span = Span(Loc(file, 3, 6), Loc(file, 3, 8)) + diagnostic = MyError(span) + run_test(source, diagnostic, snapshot, request) + + +def test_justify_line_number(snapshot, request): + source = "foo\n" * 99 + "apple == orange" + span = Span(Loc(file, 100, 6), Loc(file, 100, 8)) + diagnostic = MyError(span) + run_test(source, diagnostic, snapshot, request) + + +def test_indented(snapshot, request): + source = " " * 50 + "super_apple := apple ** 2\n" + source += " " * 50 + " lemon := orange - apple\n" + source += " " * 50 + " apple == orange" + span = Span(Loc(file, 3, 50 + 8 + 6), Loc(file, 3, 50 + 8 + 8)) + diagnostic = MyError(span) + run_test(source, diagnostic, snapshot, request) + + +def test_two_line_span(snapshot, request): + source = "apple.compare(\n orange) == EQUAL" + span = Span(Loc(file, 1, 5), Loc(file, 2, 12)) + diagnostic = MyError(span) + run_test(source, diagnostic, snapshot, request) + + +def test_three_line_span(snapshot, request): + source = "apple.compare(\n orange\n) == EQUAL" + span = Span(Loc(file, 1, 5), Loc(file, 3, 1)) + diagnostic = MyError(span) + run_test(source, diagnostic, snapshot, request) From 3dc8b87777ba0ca48f53b8dda29b14e67fe9a3aa Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Fri, 18 Oct 2024 12:21:28 +0100 Subject: [PATCH 05/20] feat: Allow emission of diagnostics inside GuppyError (#553) 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 --- guppylang/checker/expr_checker.py | 31 ++++++++++++------- guppylang/diagnostic.py | 10 ++++++ guppylang/error.py | 20 ++++++++++-- .../array_errors/new_array_elem_mismatch2.err | 13 ++++---- tests/error/inout_errors/conflict.err | 14 +++++---- .../misc_errors/implicit_module_error.err | 13 ++++---- tests/error/poly_errors/arg_mismatch1.err | 13 ++++---- tests/error/poly_errors/arg_mismatch2.err | 13 ++++---- tests/error/poly_errors/arg_mismatch3.err | 14 +++++---- .../poly_errors/inst_return_mismatch.err | 13 ++++---- .../inst_return_mismatch_nested.err | 13 ++++---- tests/error/poly_errors/return_mismatch.err | 13 ++++---- .../constructor_arg_mismatch.err | 13 ++++---- .../constructor_arg_mismatch_poly.err | 13 ++++---- tests/error/tensor_errors/type_mismatch.err | 13 ++++---- tests/error/type_errors/call_wrong_arg.err | 13 ++++---- tests/error/type_errors/fun_ty_mismatch_1.err | 14 +++++---- tests/error/type_errors/fun_ty_mismatch_2.err | 13 ++++---- tests/error/type_errors/fun_ty_mismatch_3.err | 13 ++++---- tests/error/type_errors/return_mismatch.err | 13 ++++---- .../error/type_errors/subscript_bad_item.err | 13 ++++---- 21 files changed, 177 insertions(+), 121 deletions(-) diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index d050a1b6..22beb05e 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -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, @@ -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, @@ -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. @@ -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" @@ -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: @@ -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, [] @@ -927,7 +938,7 @@ 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 @@ -935,9 +946,7 @@ def check_call( 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) diff --git a/guppylang/diagnostic.py b/guppylang/diagnostic.py index aeac49b2..e7aae617 100644 --- a/guppylang/diagnostic.py +++ b/guppylang/diagnostic.py @@ -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) diff --git a/guppylang/error.py b/guppylang/error.py index b78c9ecd..72c47683 100644 --- a/guppylang/error.py +++ b/guppylang/error.py @@ -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: @@ -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) @@ -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 "???" @@ -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: diff --git a/tests/error/array_errors/new_array_elem_mismatch2.err b/tests/error/array_errors/new_array_elem_mismatch2.err index 6cdee565..f2330dce 100644 --- a/tests/error/array_errors/new_array_elem_mismatch2.err +++ b/tests/error/array_errors/new_array_elem_mismatch2.err @@ -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 diff --git a/tests/error/inout_errors/conflict.err b/tests/error/inout_errors/conflict.err index 7fbf05fe..31a6c8f7 100644 --- a/tests/error/inout_errors/conflict.err +++ b/tests/error/inout_errors/conflict.err @@ -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 diff --git a/tests/error/misc_errors/implicit_module_error.err b/tests/error/misc_errors/implicit_module_error.err index c2afc3f8..c7d099a4 100644 --- a/tests/error/misc_errors/implicit_module_error.err +++ b/tests/error/misc_errors/implicit_module_error.err @@ -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 diff --git a/tests/error/poly_errors/arg_mismatch1.err b/tests/error/poly_errors/arg_mismatch1.err index 211407eb..e52aba0c 100644 --- a/tests/error/poly_errors/arg_mismatch1.err +++ b/tests/error/poly_errors/arg_mismatch1.err @@ -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 diff --git a/tests/error/poly_errors/arg_mismatch2.err b/tests/error/poly_errors/arg_mismatch2.err index 13b54199..8109f235 100644 --- a/tests/error/poly_errors/arg_mismatch2.err +++ b/tests/error/poly_errors/arg_mismatch2.err @@ -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 diff --git a/tests/error/poly_errors/arg_mismatch3.err b/tests/error/poly_errors/arg_mismatch3.err index 4afde1b9..d2da0e68 100644 --- a/tests/error/poly_errors/arg_mismatch3.err +++ b/tests/error/poly_errors/arg_mismatch3.err @@ -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 diff --git a/tests/error/poly_errors/inst_return_mismatch.err b/tests/error/poly_errors/inst_return_mismatch.err index 90683da3..464d5770 100644 --- a/tests/error/poly_errors/inst_return_mismatch.err +++ b/tests/error/poly_errors/inst_return_mismatch.err @@ -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 diff --git a/tests/error/poly_errors/inst_return_mismatch_nested.err b/tests/error/poly_errors/inst_return_mismatch_nested.err index 1c14fb58..30c543ba 100644 --- a/tests/error/poly_errors/inst_return_mismatch_nested.err +++ b/tests/error/poly_errors/inst_return_mismatch_nested.err @@ -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 diff --git a/tests/error/poly_errors/return_mismatch.err b/tests/error/poly_errors/return_mismatch.err index 4d3029b2..3eee2a23 100644 --- a/tests/error/poly_errors/return_mismatch.err +++ b/tests/error/poly_errors/return_mismatch.err @@ -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 diff --git a/tests/error/struct_errors/constructor_arg_mismatch.err b/tests/error/struct_errors/constructor_arg_mismatch.err index fffbb46f..97ed1e40 100644 --- a/tests/error/struct_errors/constructor_arg_mismatch.err +++ b/tests/error/struct_errors/constructor_arg_mismatch.err @@ -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 diff --git a/tests/error/struct_errors/constructor_arg_mismatch_poly.err b/tests/error/struct_errors/constructor_arg_mismatch_poly.err index 15308959..e715c8fa 100644 --- a/tests/error/struct_errors/constructor_arg_mismatch_poly.err +++ b/tests/error/struct_errors/constructor_arg_mismatch_poly.err @@ -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 diff --git a/tests/error/tensor_errors/type_mismatch.err b/tests/error/tensor_errors/type_mismatch.err index cfc31e7e..de327475 100644 --- a/tests/error/tensor_errors/type_mismatch.err +++ b/tests/error/tensor_errors/type_mismatch.err @@ -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 diff --git a/tests/error/type_errors/call_wrong_arg.err b/tests/error/type_errors/call_wrong_arg.err index 8642b137..19e01400 100644 --- a/tests/error/type_errors/call_wrong_arg.err +++ b/tests/error/type_errors/call_wrong_arg.err @@ -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 diff --git a/tests/error/type_errors/fun_ty_mismatch_1.err b/tests/error/type_errors/fun_ty_mismatch_1.err index a21337a1..26301a2c 100644 --- a/tests/error/type_errors/fun_ty_mismatch_1.err +++ b/tests/error/type_errors/fun_ty_mismatch_1.err @@ -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 diff --git a/tests/error/type_errors/fun_ty_mismatch_2.err b/tests/error/type_errors/fun_ty_mismatch_2.err index ee3f83f4..9e9437f0 100644 --- a/tests/error/type_errors/fun_ty_mismatch_2.err +++ b/tests/error/type_errors/fun_ty_mismatch_2.err @@ -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 diff --git a/tests/error/type_errors/fun_ty_mismatch_3.err b/tests/error/type_errors/fun_ty_mismatch_3.err index 93045943..d25b8e2a 100644 --- a/tests/error/type_errors/fun_ty_mismatch_3.err +++ b/tests/error/type_errors/fun_ty_mismatch_3.err @@ -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 diff --git a/tests/error/type_errors/return_mismatch.err b/tests/error/type_errors/return_mismatch.err index 0d9a0605..938d13cf 100644 --- a/tests/error/type_errors/return_mismatch.err +++ b/tests/error/type_errors/return_mismatch.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Type mismatch (at $FILE:6:11) + | +4 | @compile_guppy +5 | def foo() -> bool: +6 | return 42 + | ^^ Expected return value of type `bool`, got `int` -4: @compile_guppy -5: def foo() -> bool: -6: return 42 - ^^ -GuppyTypeError: Expected return value of type `bool`, got `int` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/subscript_bad_item.err b/tests/error/type_errors/subscript_bad_item.err index 7337c3b0..1b9fc125 100644 --- a/tests/error/type_errors/subscript_bad_item.err +++ b/tests/error/type_errors/subscript_bad_item.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Type mismatch (at $FILE:10:14) + | + 8 | @guppy(module) + 9 | def foo(xs: array[int, 42]) -> int: +10 | return xs[1.0] + | ^^^ Expected argument of type `int`, got `float` -8: @guppy(module) -9: def foo(xs: array[int, 42]) -> int: -10: return xs[1.0] - ^^^ -GuppyTypeError: Expected argument of type `int`, got `float` +Guppy compilation failed due to 1 previous error From 735020aa117119e0bde36d5a5e2e2949827a39a1 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:35:56 +0000 Subject: [PATCH 06/20] fix: Use whole cell when storing source for functions in jupyter (#586) --- guppylang/definition/function.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/guppylang/definition/function.py b/guppylang/definition/function.py index f0dbb1fc..747db20d 100644 --- a/guppylang/definition/function.py +++ b/guppylang/definition/function.py @@ -241,7 +241,13 @@ def parse_py_func(f: PyFunc, sources: SourceMap) -> tuple[ast.FunctionDef, str | defn = find_ipython_def(func_ast.name) if defn is not None: file = f"<{defn.cell_name}>" - sources.add_file(file, source) + sources.add_file(file, defn.cell_source) + else: + # If we couldn't find the defining cell, just use the source code we + # got from inspect. Line numbers will be wrong, but that's the best we + # can do. + sources.add_file(file, source) + line_offset = 1 else: file = inspect.getsourcefile(f) if file is None: From 89c36d060490f00c104af8a0f5735516fea13318 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:44:26 +0000 Subject: [PATCH 07/20] fix: Take all spans into account when determining snippet alignment (#588) --- guppylang/diagnostic.py | 15 +++++++++++++-- .../test_two_spans_different_lineno_lens.txt | 9 +++++++++ tests/diagnostics/test_diagnostics_rendering.py | 13 +++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 tests/diagnostics/snapshots/test_two_spans_different_lineno_lens.txt diff --git a/guppylang/diagnostic.py b/guppylang/diagnostic.py index e7aae617..e076956f 100644 --- a/guppylang/diagnostic.py +++ b/guppylang/diagnostic.py @@ -208,10 +208,15 @@ def render_diagnostic(self, diag: Diagnostic) -> None: else: span = to_span(diag.span) level = self.level_str(diag.level) + all_spans = [span] + [ + to_span(child.span) for child in diag.children if child.span + ] + max_lineno = max(s.end.line for s in all_spans) self.buffer.append(f"{level}: {diag.rendered_title} (at {span.start})") self.render_snippet( span, diag.rendered_span_label, + max_lineno, is_primary=True, prefix_lines=self.PREFIX_CONTEXT_LINES, ) @@ -221,6 +226,7 @@ def render_diagnostic(self, diag: Diagnostic) -> None: self.render_snippet( to_span(sub_diag.span), sub_diag.rendered_span_label, + max_lineno, is_primary=False, ) if diag.message: @@ -238,7 +244,12 @@ def render_diagnostic(self, diag: Diagnostic) -> None: ) def render_snippet( - self, span: Span, label: str | None, is_primary: bool, prefix_lines: int = 0 + self, + span: Span, + label: str | None, + max_lineno: int, + is_primary: bool, + prefix_lines: int = 0, ) -> None: """Renders the source associated with a span together with an optional label. @@ -272,7 +283,7 @@ def render_snippet( additional context. """ # Check how much space we need to reserve for the leading line numbers - ll_length = len(str(span.end.line)) + ll_length = len(str(max_lineno)) highlight_char = "^" if is_primary else "-" def render_line(line: str, line_number: int | None = None) -> None: diff --git a/tests/diagnostics/snapshots/test_two_spans_different_lineno_lens.txt b/tests/diagnostics/snapshots/test_two_spans_different_lineno_lens.txt new file mode 100644 index 00000000..d0c183a1 --- /dev/null +++ b/tests/diagnostics/snapshots/test_two_spans_different_lineno_lens.txt @@ -0,0 +1,9 @@ +Error: Can't compare apples with oranges (at :99:6) + | + 97 | apple == orange + 98 | apple == orange + 99 | apple == orange + | ^^ Comparison attempted here + | +100 | This apple is on another line + | ----- This is an apple \ No newline at end of file diff --git a/tests/diagnostics/test_diagnostics_rendering.py b/tests/diagnostics/test_diagnostics_rendering.py index aaa7ff22..90188cba 100644 --- a/tests/diagnostics/test_diagnostics_rendering.py +++ b/tests/diagnostics/test_diagnostics_rendering.py @@ -146,6 +146,19 @@ def test_justify_line_number(snapshot, request): run_test(source, diagnostic, snapshot, request) +def test_two_spans_different_lineno_lens(snapshot, request): + @dataclass(frozen=True) + class MySubDiagnostic(Note): + span_label: ClassVar[str] = "This is an apple" + + source = 99 * "apple == orange\n" + "This apple is on another line" + span1 = Span(Loc(file, 99, 6), Loc(file, 99, 8)) + span2 = Span(Loc(file, 100, 5), Loc(file, 100, 10)) + diagnostic = MyError(span1) + diagnostic.add_sub_diagnostic(MySubDiagnostic(span2)) + run_test(source, diagnostic, snapshot, request) + + def test_indented(snapshot, request): source = " " * 50 + "super_apple := apple ** 2\n" source += " " * 50 + " lemon := orange - apple\n" From e096c5367f4776ec750f1a369eaa06481ba64464 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:03:31 +0000 Subject: [PATCH 08/20] feat: Update CFG checker to use new diagnostics (#589) --- examples/demo.ipynb | 35 +++--- guppylang/cfg/builder.py | 2 +- guppylang/cfg/cfg.py | 14 +++ guppylang/checker/cfg_checker.py | 100 +++++++++++++++--- .../error/errors_on_usage/and_not_defined.err | 16 +-- .../errors_on_usage/else_expr_not_defined.err | 16 +-- .../errors_on_usage/else_expr_type_change.err | 19 ++-- .../errors_on_usage/else_not_defined.err | 16 +-- .../errors_on_usage/else_type_change.err | 19 ++-- tests/error/errors_on_usage/for_new_var.err | 16 +-- tests/error/errors_on_usage/for_target.err | 16 +-- .../for_target_type_change.err | 19 ++-- .../error/errors_on_usage/for_type_change.err | 19 ++-- .../errors_on_usage/if_different_types.err | 19 ++-- .../if_expr_cond_type_change.err | 19 ++-- .../errors_on_usage/if_expr_not_defined.err | 16 +-- .../errors_on_usage/if_expr_type_change.err | 19 ++-- .../errors_on_usage/if_expr_type_conflict.err | 19 ++-- .../error/errors_on_usage/if_not_defined.err | 16 +-- .../error/errors_on_usage/if_type_change.err | 19 ++-- .../error/errors_on_usage/or_not_defined.err | 16 +-- tests/error/errors_on_usage/while_new_var.err | 16 +-- .../errors_on_usage/while_type_change.err | 19 ++-- tests/error/misc_errors/undefined_var.err | 13 +-- .../nested_errors/different_types_if.err | 23 ++-- tests/error/nested_errors/not_defined_if.err | 16 +-- tests/error/nested_errors/var_not_defined.err | 13 +-- 27 files changed, 383 insertions(+), 167 deletions(-) diff --git a/examples/demo.ipynb b/examples/demo.ipynb index 96d84404..bafce326 100644 --- a/examples/demo.ipynb +++ b/examples/demo.ipynb @@ -203,13 +203,17 @@ "name": "stderr", "output_type": "stream", "text": [ - "Guppy compilation failed. Error in file :7\n", + "Error: Variable not defined (at :7:11)\n", + " | \n", + "5 | if b:\n", + "6 | x = 4\n", + "7 | return x # x not defined if b is False\n", + " | ^ `x` might be undefined ...\n", + " | \n", + "5 | if b:\n", + " | - ... if this expression is `False`\n", "\n", - "5: if b:\n", - "6: x = 4\n", - "7: return x # x not defined if b is False\n", - " ^\n", - "GuppyError: Variable `x` is not defined on all control-flow paths.\n" + "Guppy compilation failed due to 1 previous error\n" ] } ], @@ -247,13 +251,20 @@ "name": "stderr", "output_type": "stream", "text": [ - "Guppy compilation failed. Error in file :9\n", + "Error: Different types (at :9:15)\n", + " | \n", + "7 | else:\n", + "8 | x = True\n", + "9 | return int(x) # x has different types depending on b\n", + " | ^ Variable `x` may refer to different types\n", + " | \n", + "6 | x = 4\n", + " | - This is of type `int`\n", + " | \n", + "8 | x = True\n", + " | - This is of type `bool`\n", "\n", - "7: else:\n", - "8: x = True\n", - "9: return int(x) # x has different types depending on b\n", - " ^\n", - "GuppyError: Variable `x` can refer to different types: `int` (at 6:8) vs `bool` (at 8:8)\n" + "Guppy compilation failed due to 1 previous error\n" ] } ], diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index c781088f..cd76fb8c 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -177,7 +177,7 @@ def visit_For(self, node: ast.For, bb: BB, jumps: Jumps) -> BB | None: b = make_var(next(tmp_vars), node.iter) new_nodes = template_replace( template, - node, + node.iter, it=it, b=b, x=node.target, diff --git a/guppylang/cfg/cfg.py b/guppylang/cfg/cfg.py index e84b2dc7..ff408fca 100644 --- a/guppylang/cfg/cfg.py +++ b/guppylang/cfg/cfg.py @@ -1,3 +1,5 @@ +from collections import deque +from collections.abc import Iterator from typing import Generic, TypeVar from guppylang.cfg.analysis import ( @@ -37,6 +39,18 @@ def __init__( self.ass_before = {} self.maybe_ass_before = {} + def ancestors(self, *bbs: T) -> Iterator[T]: + """Returns an iterator over all ancestors of the given BBs in BFS order.""" + queue = deque(bbs) + visited = set() + while queue: + bb = queue.popleft() + if bb in visited: + continue + visited.add(bb) + yield bb + queue += bb.predecessors + class CFG(BaseCFG[BB]): """A control-flow graph of unchecked basic blocks.""" diff --git a/guppylang/checker/cfg_checker.py b/guppylang/checker/cfg_checker.py index 0f7a4e53..fbd748b9 100644 --- a/guppylang/checker/cfg_checker.py +++ b/guppylang/checker/cfg_checker.py @@ -4,10 +4,11 @@ `CheckedBB`s with inferred type signatures. """ +import ast import collections from collections.abc import Iterator, Sequence from dataclasses import dataclass, field -from typing import Generic, TypeVar +from typing import ClassVar, Generic, TypeVar from guppylang.ast_util import line_col from guppylang.cfg.bb import BB @@ -15,6 +16,7 @@ from guppylang.checker.core import Context, Globals, Locals, Place, V, Variable from guppylang.checker.expr_checker import ExprSynthesizer, to_bool from guppylang.checker.stmt_checker import StmtChecker +from guppylang.diagnostic import Error, Note from guppylang.error import GuppyError from guppylang.tys.ty import InputFlags, Type @@ -129,6 +131,44 @@ def check_cfg( return linearity_checked_cfg +@dataclass(frozen=True) +class VarNotDefinedError(Error): + title: ClassVar[str] = "Variable not defined" + span_label: ClassVar[str] = "`{var}` is not defined" + var: str + + +@dataclass(frozen=True) +class VarMaybeNotDefinedError(Error): + title: ClassVar[str] = "Variable not defined" + var: str + + @dataclass(frozen=True) + class BadBranch(Note): + span_label: ClassVar[str] = "... if this expression is `{truth_value}`" + var: str + truth_value: bool + + @property + def rendered_span_label(self) -> str: + s = f"`{self.var}` might be undefined" + if self.children: + s += " ..." + return s + + +@dataclass(frozen=True) +class BranchTypeError(Error): + title: ClassVar[str] = "Different types" + span_label: ClassVar[str] = "{ident} may refer to different types" + ident: str + + @dataclass(frozen=True) + class TypeHint(Note): + span_label: ClassVar[str] = "This is of type `{ty}`" + ty: Type + + def check_bb( bb: BB, checked_cfg: CheckedCFG[Variable], @@ -144,7 +184,7 @@ def check_bb( assert len(bb.predecessors) == 0 for x, use in bb.vars.used.items(): if x not in cfg.ass_before[bb] and x not in globals: - raise GuppyError(f"Variable `{x}` is not defined", use) + raise GuppyError(VarNotDefinedError(use, x)) # Check the basic block ctx = Context(globals, Locals({v.name: v for v in inputs})) @@ -163,14 +203,15 @@ def check_bb( # If the variable is defined on *some* paths, we can give a more # informative error message if x in cfg.maybe_ass_before[use_bb]: - # TODO: This should be "Variable x is not defined when coming - # from {bb}". But for this we need a way to associate BBs with - # source locations. - raise GuppyError( - f"Variable `{x}` is not defined on all control-flow paths.", - use_bb.vars.used[x], - ) - raise GuppyError(f"Variable `{x}` is not defined", use_bb.vars.used[x]) + err = VarMaybeNotDefinedError(use_bb.vars.used[x], x) + if bad_branch := diagnose_maybe_undefined(use_bb, x, cfg): + branch_expr, truth_value = bad_branch + note = VarMaybeNotDefinedError.BadBranch( + branch_expr, x, truth_value + ) + err.add_sub_diagnostic(note) + raise GuppyError(err) + raise GuppyError(VarNotDefinedError(use_bb.vars.used[x], x)) # Finally, we need to compute the signature of the basic block outputs = [ @@ -209,12 +250,39 @@ def check_rows_match(row1: Row[Variable], row2: Row[Variable], bb: BB) -> None: # We shouldn't mention temporary variables (starting with `%`) # in error messages: ident = "Expression" if v1.name.startswith("%") else f"Variable `{v1.name}`" - raise GuppyError( - f"{ident} can refer to different types: " - f"`{v1.ty}` (at {{}}) vs `{v2.ty}` (at {{}})", - bb.containing_cfg.live_before[bb][v1.name].vars.used[v1.name], - [v1.defined_at, v2.defined_at], - ) + use = bb.containing_cfg.live_before[bb][v1.name].vars.used[v1.name] + err = BranchTypeError(use, ident) + err.add_sub_diagnostic(BranchTypeError.TypeHint(v1.defined_at, v1.ty)) + err.add_sub_diagnostic(BranchTypeError.TypeHint(v2.defined_at, v2.ty)) + raise GuppyError(err) + + +def diagnose_maybe_undefined( + bb: BB, x: str, cfg: BaseCFG[BB] +) -> tuple[ast.expr, bool] | None: + """Given a BB and a variable `x`, tries to find a branch where one of the successors + leads to an assignment of `x` while the other one does not. + + Returns the branch condition and a flag whether the value being `True` leads to the + undefined path. Returns `None` if no such branch can be found. + """ + assert x in cfg.maybe_ass_before[bb] + # Find all BBs that can reach this BB and which ones of those assign `x` + ancestors = list(cfg.ancestors(bb)) + assigns = [anc for anc in ancestors if x in anc.vars.assigned] + # Compute which ancestors can possibly reach an assignment + reaches_assignment = set(cfg.ancestors(*assigns)) + # Try to find a branching BB where one of paths can reach an assignment, while the + # other one cannot + for anc in ancestors: + match anc.successors: + case [true_succ, false_succ]: + assert anc.branch_pred is not None + true_reaches_assignment = true_succ in reaches_assignment + false_reaches_assignment = false_succ in reaches_assignment + if true_reaches_assignment != false_reaches_assignment: + return anc.branch_pred, true_reaches_assignment + return None T = TypeVar("T") diff --git a/tests/error/errors_on_usage/and_not_defined.err b/tests/error/errors_on_usage/and_not_defined.err index cf3b2169..2466d305 100644 --- a/tests/error/errors_on_usage/and_not_defined.err +++ b/tests/error/errors_on_usage/and_not_defined.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Variable not defined (at $FILE:9:15) + | +7 | return z +8 | else: +9 | return z + | ^ `z` might be undefined ... + | +6 | if x and (z := y + 1): + | - ... if this expression is `False` -7: return z -8: else: -9: return z - ^ -GuppyError: Variable `z` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/else_expr_not_defined.err b/tests/error/errors_on_usage/else_expr_not_defined.err index 91c437ae..3a0444b5 100644 --- a/tests/error/errors_on_usage/else_expr_not_defined.err +++ b/tests/error/errors_on_usage/else_expr_not_defined.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Variable not defined (at $FILE:7:11) + | +5 | def foo(x: bool) -> int: +6 | (y := 1) if x else (z := 2) +7 | return z + | ^ `z` might be undefined ... + | +6 | (y := 1) if x else (z := 2) + | - ... if this expression is `True` -5: def foo(x: bool) -> int: -6: (y := 1) if x else (z := 2) -7: return z - ^ -GuppyError: Variable `z` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/else_expr_type_change.err b/tests/error/errors_on_usage/else_expr_type_change.err index 6a8560f2..711bb275 100644 --- a/tests/error/errors_on_usage/else_expr_type_change.err +++ b/tests/error/errors_on_usage/else_expr_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Different types (at $FILE:8:11) + | +6 | y = 3 +7 | (y := y + 1) if x else (y := True) +8 | return y + | ^ Variable `y` may refer to different types + | +7 | (y := y + 1) if x else (y := True) + | - This is of type `int` + | +7 | (y := y + 1) if x else (y := True) + | - This is of type `bool` -6: y = 3 -7: (y := y + 1) if x else (y := True) -8: return y - ^ -GuppyError: Variable `y` can refer to different types: `int` (at 7:5) vs `bool` (at 7:28) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/else_not_defined.err b/tests/error/errors_on_usage/else_not_defined.err index 790e5d65..34b4d644 100644 --- a/tests/error/errors_on_usage/else_not_defined.err +++ b/tests/error/errors_on_usage/else_not_defined.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Variable not defined (at $FILE:10:11) + | + 8 | else: + 9 | z = 2 +10 | return z + | ^ `z` might be undefined ... + | + 6 | if x: + | - ... if this expression is `True` -8: else: -9: z = 2 -10: return z - ^ -GuppyError: Variable `z` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/else_type_change.err b/tests/error/errors_on_usage/else_type_change.err index 00e80e79..07c195a6 100644 --- a/tests/error/errors_on_usage/else_type_change.err +++ b/tests/error/errors_on_usage/else_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Different types (at $FILE:11:11) + | + 9 | else: +10 | y = True +11 | return y + | ^ Variable `y` may refer to different types + | + 8 | y += 1 + | - This is of type `int` + | +10 | y = True + | - This is of type `bool` -9: else: -10: y = True -11: return y - ^ -GuppyError: Variable `y` can refer to different types: `int` (at 8:8) vs `bool` (at 10:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/for_new_var.err b/tests/error/errors_on_usage/for_new_var.err index 7aae5692..978aa7ee 100644 --- a/tests/error/errors_on_usage/for_new_var.err +++ b/tests/error/errors_on_usage/for_new_var.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Variable not defined (at $FILE:8:11) + | +6 | for _ in xs: +7 | y = 5 +8 | return y + | ^ `y` might be undefined ... + | +6 | for _ in xs: + | -- ... if this expression is `False` -6: for _ in xs: -7: y = 5 -8: return y - ^ -GuppyError: Variable `y` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/for_target.err b/tests/error/errors_on_usage/for_target.err index 30602c60..422c59f7 100644 --- a/tests/error/errors_on_usage/for_target.err +++ b/tests/error/errors_on_usage/for_target.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Variable not defined (at $FILE:8:11) + | +6 | for x in xs: +7 | pass +8 | return x + | ^ `x` might be undefined ... + | +6 | for x in xs: + | -- ... if this expression is `False` -6: for x in xs: -7: pass -8: return x - ^ -GuppyError: Variable `x` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/for_target_type_change.err b/tests/error/errors_on_usage/for_target_type_change.err index 82bffc0e..4dbacc16 100644 --- a/tests/error/errors_on_usage/for_target_type_change.err +++ b/tests/error/errors_on_usage/for_target_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Different types (at $FILE:9:11) + | +7 | for x in xs: +8 | pass +9 | return x + | ^ Variable `x` may refer to different types + | +6 | x = 5 + | - This is of type `int` + | +7 | for x in xs: + | - This is of type `bool` -7: for x in xs: -8: pass -9: return x - ^ -GuppyError: Variable `x` can refer to different types: `int` (at 6:4) vs `bool` (at 7:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/for_type_change.err b/tests/error/errors_on_usage/for_type_change.err index 8b4fb557..09e7371b 100644 --- a/tests/error/errors_on_usage/for_type_change.err +++ b/tests/error/errors_on_usage/for_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Different types (at $FILE:9:11) + | +7 | for x in xs: +8 | y = True +9 | return y + | ^ Variable `y` may refer to different types + | +6 | y = 5 + | - This is of type `int` + | +8 | y = True + | - This is of type `bool` -7: for x in xs: -8: y = True -9: return y - ^ -GuppyError: Variable `y` can refer to different types: `int` (at 6:4) vs `bool` (at 8:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/if_different_types.err b/tests/error/errors_on_usage/if_different_types.err index a9fe3aeb..8fc7817a 100644 --- a/tests/error/errors_on_usage/if_different_types.err +++ b/tests/error/errors_on_usage/if_different_types.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Different types (at $FILE:10:11) + | + 8 | else: + 9 | y = False +10 | return y + | ^ Variable `y` may refer to different types + | + 7 | y = 1 + | - This is of type `int` + | + 9 | y = False + | - This is of type `bool` -8: else: -9: y = False -10: return y - ^ -GuppyError: Variable `y` can refer to different types: `int` (at 7:8) vs `bool` (at 9:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/if_expr_cond_type_change.err b/tests/error/errors_on_usage/if_expr_cond_type_change.err index a14cdcf8..4e8d692f 100644 --- a/tests/error/errors_on_usage/if_expr_cond_type_change.err +++ b/tests/error/errors_on_usage/if_expr_cond_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Different types (at $FILE:8:8) + | +6 | y = 4 +7 | 0 if (y := x) else (y := 6) +8 | z = y + | ^ Variable `y` may refer to different types + | +7 | 0 if (y := x) else (y := 6) + | - This is of type `bool` + | +7 | 0 if (y := x) else (y := 6) + | - This is of type `int` -6: y = 4 -7: 0 if (y := x) else (y := 6) -8: z = y - ^ -GuppyError: Variable `y` can refer to different types: `bool` (at 7:10) vs `int` (at 7:24) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/if_expr_not_defined.err b/tests/error/errors_on_usage/if_expr_not_defined.err index 1441eb28..6c4ba0cb 100644 --- a/tests/error/errors_on_usage/if_expr_not_defined.err +++ b/tests/error/errors_on_usage/if_expr_not_defined.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Variable not defined (at $FILE:7:11) + | +5 | def foo(x: bool) -> int: +6 | (y := 1) if x else 0 +7 | return y + | ^ `y` might be undefined ... + | +6 | (y := 1) if x else 0 + | - ... if this expression is `False` -5: def foo(x: bool) -> int: -6: (y := 1) if x else 0 -7: return y - ^ -GuppyError: Variable `y` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/if_expr_type_change.err b/tests/error/errors_on_usage/if_expr_type_change.err index ce293991..f24e6b10 100644 --- a/tests/error/errors_on_usage/if_expr_type_change.err +++ b/tests/error/errors_on_usage/if_expr_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Different types (at $FILE:8:8) + | +6 | y = 3 +7 | (y := False) if x or a > 5 else 0 +8 | z = y + | ^ Variable `y` may refer to different types + | +6 | y = 3 + | - This is of type `int` + | +7 | (y := False) if x or a > 5 else 0 + | - This is of type `bool` -6: y = 3 -7: (y := False) if x or a > 5 else 0 -8: z = y - ^ -GuppyError: Variable `y` can refer to different types: `int` (at 6:4) vs `bool` (at 7:5) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/if_expr_type_conflict.err b/tests/error/errors_on_usage/if_expr_type_conflict.err index 239b9059..00ad0e43 100644 --- a/tests/error/errors_on_usage/if_expr_type_conflict.err +++ b/tests/error/errors_on_usage/if_expr_type_conflict.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Different types (at $FILE:6:8) + | +4 | @compile_guppy +5 | def foo(x: bool) -> None: +6 | y = True if x else 42 + | ^^^^^^^^^^^^^^^^^ Expression may refer to different types + | +6 | y = True if x else 42 + | ---- This is of type `bool` + | +6 | y = True if x else 42 + | -- This is of type `int` -4: @compile_guppy -5: def foo(x: bool) -> None: -6: y = True if x else 42 - ^^^^^^^^^^^^^^^^^ -GuppyError: Expression can refer to different types: `bool` (at 6:8) vs `int` (at 6:23) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/if_not_defined.err b/tests/error/errors_on_usage/if_not_defined.err index f4618108..6624c374 100644 --- a/tests/error/errors_on_usage/if_not_defined.err +++ b/tests/error/errors_on_usage/if_not_defined.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Variable not defined (at $FILE:8:11) + | +6 | if x: +7 | y = 1 +8 | return y + | ^ `y` might be undefined ... + | +6 | if x: + | - ... if this expression is `False` -6: if x: -7: y = 1 -8: return y - ^ -GuppyError: Variable `y` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/if_type_change.err b/tests/error/errors_on_usage/if_type_change.err index 297b15dc..daf45dc9 100644 --- a/tests/error/errors_on_usage/if_type_change.err +++ b/tests/error/errors_on_usage/if_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Different types (at $FILE:9:8) + | +7 | if x: +8 | y = False +9 | z = y + | ^ Variable `y` may refer to different types + | +6 | y = 3 + | - This is of type `int` + | +8 | y = False + | - This is of type `bool` -7: if x: -8: y = False -9: z = y - ^ -GuppyError: Variable `y` can refer to different types: `int` (at 6:4) vs `bool` (at 8:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/or_not_defined.err b/tests/error/errors_on_usage/or_not_defined.err index 11d9f692..d4d95ac8 100644 --- a/tests/error/errors_on_usage/or_not_defined.err +++ b/tests/error/errors_on_usage/or_not_defined.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Variable not defined (at $FILE:7:15) + | +5 | def foo(x: bool, y: int) -> int: +6 | if x or (z := y + 1): +7 | return z + | ^ `z` might be undefined ... + | +6 | if x or (z := y + 1): + | - ... if this expression is `True` -5: def foo(x: bool, y: int) -> int: -6: if x or (z := y + 1): -7: return z - ^ -GuppyError: Variable `z` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/while_new_var.err b/tests/error/errors_on_usage/while_new_var.err index 2125442b..131c00ca 100644 --- a/tests/error/errors_on_usage/while_new_var.err +++ b/tests/error/errors_on_usage/while_new_var.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Variable not defined (at $FILE:8:11) + | +6 | while x: +7 | y = 5 +8 | return y + | ^ `y` might be undefined ... + | +6 | while x: + | - ... if this expression is `False` -6: while x: -7: y = 5 -8: return y - ^ -GuppyError: Variable `y` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/errors_on_usage/while_type_change.err b/tests/error/errors_on_usage/while_type_change.err index 2317fe55..36680d65 100644 --- a/tests/error/errors_on_usage/while_type_change.err +++ b/tests/error/errors_on_usage/while_type_change.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Different types (at $FILE:9:11) + | +7 | while x: +8 | y = True +9 | return y + | ^ Variable `y` may refer to different types + | +6 | y = 5 + | - This is of type `int` + | +8 | y = True + | - This is of type `bool` -7: while x: -8: y = True -9: return y - ^ -GuppyError: Variable `y` can refer to different types: `int` (at 6:4) vs `bool` (at 8:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/undefined_var.err b/tests/error/misc_errors/undefined_var.err index d5cf6925..b96143c0 100644 --- a/tests/error/misc_errors/undefined_var.err +++ b/tests/error/misc_errors/undefined_var.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Variable not defined (at $FILE:6:11) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | return x + | ^ `x` is not defined -4: @compile_guppy -5: def foo() -> int: -6: return x - ^ -GuppyError: Variable `x` is not defined +Guppy compilation failed due to 1 previous error diff --git a/tests/error/nested_errors/different_types_if.err b/tests/error/nested_errors/different_types_if.err index da397a08..0f75dffd 100644 --- a/tests/error/nested_errors/different_types_if.err +++ b/tests/error/nested_errors/different_types_if.err @@ -1,7 +1,18 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Different types (at $FILE:13:11) + | +11 | return False +12 | +13 | return bar() + | ^^^ Variable `bar` may refer to different types + | + 7 | def bar() -> int: + | ----------------- + 8 | return 0 + | -------------------- This is of type `() -> int` + | +10 | def bar() -> bool: + | ------------------ +11 | return False + | ------------------------ This is of type `() -> bool` -11: return False -12: -13: return bar() - ^^^ -GuppyError: Variable `bar` can refer to different types: `() -> int` (at 7:8) vs `() -> bool` (at 10:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/nested_errors/not_defined_if.err b/tests/error/nested_errors/not_defined_if.err index 811a1b08..8d922f4c 100644 --- a/tests/error/nested_errors/not_defined_if.err +++ b/tests/error/nested_errors/not_defined_if.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Variable not defined (at $FILE:9:11) + | +7 | def bar() -> int: +8 | return 0 +9 | return bar() + | ^^^ `bar` might be undefined ... + | +6 | if b: + | - ... if this expression is `False` -7: def bar() -> int: -8: return 0 -9: return bar() - ^^^ -GuppyError: Variable `bar` is not defined on all control-flow paths. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/nested_errors/var_not_defined.err b/tests/error/nested_errors/var_not_defined.err index eaf9f7b2..a9f2af56 100644 --- a/tests/error/nested_errors/var_not_defined.err +++ b/tests/error/nested_errors/var_not_defined.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Variable not defined (at $FILE:7:15) + | +5 | def foo() -> int: +6 | def bar() -> int: +7 | return x + | ^ `x` is not defined -5: def foo() -> int: -6: def bar() -> int: -7: return x - ^ -GuppyError: Variable `x` is not defined +Guppy compilation failed due to 1 previous error From 16d84b51b780e1b3fb2af92416f3fc80b0d40025 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:38:04 +0000 Subject: [PATCH 09/20] feat: Allow sub-diagnostic labels to refer to fields of their parent (#600) Sub-diagnostics now store a pointer to their parent main diagnostic that is used to look up field values when formatting labels and messages. This is achieved by subclassing `string.Formatter` to implement the custom lookup --- guppylang/diagnostic.py | 31 +++++++++++++++--- .../snapshots/test_advanced_formatting.txt | 7 ++++ .../diagnostics/test_diagnostics_rendering.py | 32 +++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/diagnostics/snapshots/test_advanced_formatting.txt diff --git a/guppylang/diagnostic.py b/guppylang/diagnostic.py index e076956f..0983a31b 100644 --- a/guppylang/diagnostic.py +++ b/guppylang/diagnostic.py @@ -1,7 +1,17 @@ +import string import textwrap -from dataclasses import dataclass, field, fields +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field from enum import Enum, auto -from typing import ClassVar, Final, Literal, Protocol, overload, runtime_checkable +from typing import ( + Any, + ClassVar, + Final, + Literal, + Protocol, + overload, + runtime_checkable, +) from typing_extensions import Self @@ -54,6 +64,9 @@ class SubDiagnostic(Protocol): #: Message that is printed if no span is provided. message: ClassVar[str | None] = None + #: The parent main diagnostic this sub-diagnostic is attached to. + _parent: "Diagnostic | None" = field(default=None, init=False) + def __post_init__(self) -> None: if self.span_label and self.span is None: raise InternalGuppyError("SubDiagnostic: Span label provided without span") @@ -78,8 +91,17 @@ def _render(self, s: str | None) -> str | None: """Helper method to fill in placeholder values in strings with fields of this diagnostic. """ - values = {f.name: getattr(self, f.name) for f in fields(self)} - return s.format(**values) if s is not None else None + + class CustomFormatter(string.Formatter): + def get_value( + _self, key: int | str, args: Sequence[Any], kwargs: Mapping[str, Any] + ) -> Any: + assert isinstance(key, str) + if hasattr(self, key): + return getattr(self, key) + return getattr(self._parent, key) + + return CustomFormatter().format(s) if s is not None else None @runtime_checkable @@ -114,6 +136,7 @@ def add_sub_diagnostic(self, sub: "SubDiagnostic") -> Self: raise InternalGuppyError( "Diagnostic: Cross-file sub-diagnostics are not supported" ) + object.__setattr__(sub, "_parent", self) self.children.append(sub) return self diff --git a/tests/diagnostics/snapshots/test_advanced_formatting.txt b/tests/diagnostics/snapshots/test_advanced_formatting.txt new file mode 100644 index 00000000..4d2bd537 --- /dev/null +++ b/tests/diagnostics/snapshots/test_advanced_formatting.txt @@ -0,0 +1,7 @@ +Error: Can't compare apples with oranges (at :1:0) + | +1 | apple == orange + | ^^^^^ This is an apple + | +1 | apple == orange + | ------ This is not an apple \ No newline at end of file diff --git a/tests/diagnostics/test_diagnostics_rendering.py b/tests/diagnostics/test_diagnostics_rendering.py index 90188cba..639a883e 100644 --- a/tests/diagnostics/test_diagnostics_rendering.py +++ b/tests/diagnostics/test_diagnostics_rendering.py @@ -96,6 +96,38 @@ class MySubDiagnostic(Note): run_test(source, diagnostic, snapshot, request) +def test_advanced_formatting(snapshot, request): + @dataclass(frozen=True) + class MyDiagnostic(Error): + title: ClassVar[str] = "Can't compare apples with oranges" + span_label: ClassVar[str] = "This is an {a}{pp}{le}" + a: str + + @property + def pp(self) -> str: + return "pp" + + @property + def le(self) -> str: + return "le" + + @dataclass(frozen=True) + class MySubDiagnostic(Note): + span_label: ClassVar[str] = "This is not an {a}{pp}{p}{le}" + p: str + + @property + def pp(self) -> str: + return "p" + + source = "apple == orange" + span_apple = Span(Loc(file, 1, 0), Loc(file, 1, 5)) + span_orange = Span(Loc(file, 1, 9), Loc(file, 1, 15)) + diagnostic = MyDiagnostic(span_apple, "a") + diagnostic.add_sub_diagnostic(MySubDiagnostic(span_orange, "p")) + run_test(source, diagnostic, snapshot, request) + + def test_long_label(snapshot, request): @dataclass(frozen=True) class MyDiagnostic(Error): From cd73c1bb7d8e845977c0959a8fcb0412ccc93f36 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:36:48 +0000 Subject: [PATCH 10/20] refactor: Update function checker to use new diagnostics (#590) --- examples/demo.ipynb | 17 +++-- guppylang/checker/core.py | 9 +++ guppylang/checker/func_checker.py | 68 ++++++++++++++----- tests/error/misc_errors/arg_not_annotated.err | 12 ++-- .../misc_errors/return_not_annotated.err | 14 ++-- .../return_not_annotated_none1.err | 17 +++-- .../return_not_annotated_none2.err | 18 +++-- .../nested_errors/reassign_capture_1.err | 17 +++-- .../nested_errors/reassign_capture_2.err | 17 +++-- .../nested_errors/reassign_capture_3.err | 17 +++-- 10 files changed, 145 insertions(+), 61 deletions(-) diff --git a/examples/demo.ipynb b/examples/demo.ipynb index bafce326..d72e1a54 100644 --- a/examples/demo.ipynb +++ b/examples/demo.ipynb @@ -497,13 +497,18 @@ "name": "stderr", "output_type": "stream", "text": [ - "Guppy compilation failed. Error in file :6\n", + "Error: Illegal assignment (at :6:8)\n", + " | \n", + "4 | def outer(x: int) -> int:\n", + "5 | def nested() -> None:\n", + "6 | x += 1 # Mutation of captured variable x is not allowed\n", + " | ^^^^^^ Variable `x` may not be assigned to since `x` is captured\n", + " | from an outer scope\n", + " | \n", + "4 | def outer(x: int) -> int:\n", + " | ------ `x` defined here\n", "\n", - "4: def outer(x: int) -> int:\n", - "5: def nested() -> None:\n", - "6: x += 1 # Mutation of captured variable x is not allowed\n", - " ^^^^^^\n", - "GuppyError: Variable `x` defined in an outer scope (at 4:10) may not be assigned to\n" + "Guppy compilation failed due to 1 previous error\n" ] } ], diff --git a/guppylang/checker/core.py b/guppylang/checker/core.py index 4f732b13..41b3c99e 100644 --- a/guppylang/checker/core.py +++ b/guppylang/checker/core.py @@ -7,6 +7,7 @@ from typing import ( TYPE_CHECKING, Any, + ClassVar, Generic, NamedTuple, TypeAlias, @@ -20,6 +21,7 @@ from guppylang.definition.common import DefId, Definition from guppylang.definition.ty import TypeDef from guppylang.definition.value import CallableDef +from guppylang.diagnostic import Error from guppylang.tys.builtin import ( array_type_def, bool_type_def, @@ -49,6 +51,13 @@ from guppylang.definition.struct import StructField +@dataclass(frozen=True) +class UnsupportedError(Error): + title: ClassVar[str] = "Unsupported" + span_label: ClassVar[str] = "{things} are not supported" + things: str + + #: A "place" is a description for a storage location of a local value that users #: can refer to in their program. #: diff --git a/guppylang/checker/func_checker.py b/guppylang/checker/func_checker.py index 47d59b2a..7213de4a 100644 --- a/guppylang/checker/func_checker.py +++ b/guppylang/checker/func_checker.py @@ -6,14 +6,16 @@ """ import ast -from typing import TYPE_CHECKING +from dataclasses import dataclass +from typing import TYPE_CHECKING, ClassVar from guppylang.ast_util import return_nodes_in_ast, with_loc from guppylang.cfg.bb import BB from guppylang.cfg.builder import CFGBuilder from guppylang.checker.cfg_checker import CheckedCFG, check_cfg -from guppylang.checker.core import Context, Globals, Place, Variable +from guppylang.checker.core import Context, Globals, Place, UnsupportedError, Variable from guppylang.definition.common import DefId +from guppylang.diagnostic import Error, Help, Note from guppylang.error import GuppyError from guppylang.nodes import CheckedNestedFunctionDef, NestedFunctionDef from guppylang.tys.parsing import parse_function_io_types @@ -23,6 +25,41 @@ from guppylang.tys.param import Parameter +@dataclass(frozen=True) +class IllegalAssignError(Error): + title: ClassVar[str] = "Illegal assignment" + span_label: ClassVar[str] = ( + "Variable `{var}` may not be assigned to since `{var}` is captured from an " + "outer scope" + ) + var: str + + @dataclass(frozen=True) + class DefHint(Note): + span_label: ClassVar[str] = "`{var}` defined here" + var: str + + +@dataclass(frozen=True) +class MissingArgAnnotationError(Error): + title: ClassVar[str] = "Missing type annotation" + span_label: ClassVar[str] = "Argument requires a type annotation" + + +@dataclass(frozen=True) +class MissingReturnAnnotationError(Error): + title: ClassVar[str] = "Missing type annotation" + span_label: ClassVar[str] = "Return type must be annotated" + + @dataclass(frozen=True) + class ReturnNone(Help): + message: ClassVar[str] = ( + "Looks like `{func}` doesn't return anything. Consider annotating it with " + "`-> None`." + ) + func: str + + def check_global_func_def( func_def: ast.FunctionDef, ty: FunctionType, globals: Globals ) -> CheckedCFG[Place]: @@ -67,12 +104,9 @@ def check_nested_func_def( for v, _ in captured.values(): x = v.name if x in bb.vars.assigned: - raise GuppyError( - f"Variable `{x}` defined in an outer scope (at {{0}}) may not " - f"be assigned to", - bb.vars.assigned[x], - [v.defined_at], - ) + err = IllegalAssignError(bb.vars.assigned[x], x) + err.add_sub_diagnostic(IllegalAssignError.DefHint(v.defined_at, x)) + raise GuppyError(err) # Construct inputs for checking the body CFG inputs = [v for v, _ in captured.values()] + [ @@ -123,24 +157,24 @@ def check_signature(func_def: ast.FunctionDef, globals: Globals) -> FunctionType Guppy type.""" if len(func_def.args.posonlyargs) != 0: raise GuppyError( - "Positional-only parameters not supported", func_def.args.posonlyargs[0] + UnsupportedError(func_def.args.posonlyargs[0], "Positional-only parameters") ) if len(func_def.args.kwonlyargs) != 0: raise GuppyError( - "Keyword-only parameters not supported", func_def.args.kwonlyargs[0] + UnsupportedError(func_def.args.kwonlyargs[0], "Keyword-only parameters") ) if func_def.args.vararg is not None: - raise GuppyError("*args not supported", func_def.args.vararg) + raise GuppyError(UnsupportedError(func_def.args.vararg, "Variadic args")) if func_def.args.kwarg is not None: - raise GuppyError("**kwargs not supported", func_def.args.kwarg) + raise GuppyError(UnsupportedError(func_def.args.kwarg, "Keyword args")) if func_def.returns is None: + err = MissingReturnAnnotationError(func_def) # TODO: Error location is incorrect if all(r.value is None for r in return_nodes_in_ast(func_def)): - raise GuppyError( - "Return type must be annotated. Try adding a `-> None` annotation.", - func_def, + err.add_sub_diagnostic( + MissingReturnAnnotationError.ReturnNone(None, func_def.name) ) - raise GuppyError("Return type must be annotated", func_def) + raise GuppyError(err) # TODO: Prepopulate mapping when using Python 3.12 style generic functions param_var_mapping: dict[str, Parameter] = {} @@ -149,7 +183,7 @@ def check_signature(func_def: ast.FunctionDef, globals: Globals) -> FunctionType for inp in func_def.args.args: ty_ast = inp.annotation if ty_ast is None: - raise GuppyError("Argument type must be annotated", inp) + raise GuppyError(MissingArgAnnotationError(inp)) input_nodes.append(ty_ast) input_names.append(inp.arg) inputs, output = parse_function_io_types( diff --git a/tests/error/misc_errors/arg_not_annotated.err b/tests/error/misc_errors/arg_not_annotated.err index 0e680b48..550cd3ce 100644 --- a/tests/error/misc_errors/arg_not_annotated.err +++ b/tests/error/misc_errors/arg_not_annotated.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:5 +Error: Missing type annotation (at $FILE:5:17) + | +3 | +4 | @compile_guppy +5 | def foo(x: bool, y) -> int: + | ^ Argument requires a type annotation -3: @compile_guppy -4: def foo(x: bool, y) -> int: - ^ -GuppyError: Argument type must be annotated +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/return_not_annotated.err b/tests/error/misc_errors/return_not_annotated.err index e66c67e0..9e972f9e 100644 --- a/tests/error/misc_errors/return_not_annotated.err +++ b/tests/error/misc_errors/return_not_annotated.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:5 +Error: Missing type annotation (at $FILE:5:0) + | +3 | +4 | @compile_guppy +5 | def foo(x: bool): + | ^^^^^^^^^^^^^^^^^ +6 | return x + | ^^^^^^^^^^^^ Return type must be annotated -3: @compile_guppy -4: def foo(x: bool): - ^^^^^^^^^^^^^^^^^ -GuppyError: Return type must be annotated +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/return_not_annotated_none1.err b/tests/error/misc_errors/return_not_annotated_none1.err index 2798749f..0c4e5563 100644 --- a/tests/error/misc_errors/return_not_annotated_none1.err +++ b/tests/error/misc_errors/return_not_annotated_none1.err @@ -1,6 +1,13 @@ -Guppy compilation failed. Error in file $FILE:5 +Error: Missing type annotation (at $FILE:5:0) + | +3 | +4 | @compile_guppy +5 | def foo(): + | ^^^^^^^^^^ +6 | return + | ^^^^^^^^^^ Return type must be annotated -3: @compile_guppy -4: def foo(): - ^^^^^^^^^^ -GuppyError: Return type must be annotated. Try adding a `-> None` annotation. +Help: Looks like `foo` doesn't return anything. Consider annotating it with `-> +None`. + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/return_not_annotated_none2.err b/tests/error/misc_errors/return_not_annotated_none2.err index 2798749f..b6974fc3 100644 --- a/tests/error/misc_errors/return_not_annotated_none2.err +++ b/tests/error/misc_errors/return_not_annotated_none2.err @@ -1,6 +1,14 @@ -Guppy compilation failed. Error in file $FILE:5 +Error: Missing type annotation (at $FILE:5:0) + | +3 | +4 | @compile_guppy +5 | def foo(): + | ^^^^^^^^^^ + | ... +7 | return x + | ^^^^^^^^^^^^^^^^ Return type must be annotated -3: @compile_guppy -4: def foo(): - ^^^^^^^^^^ -GuppyError: Return type must be annotated. Try adding a `-> None` annotation. +Help: Looks like `foo` doesn't return anything. Consider annotating it with `-> +None`. + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/nested_errors/reassign_capture_1.err b/tests/error/nested_errors/reassign_capture_1.err index e3244d25..da2272a9 100644 --- a/tests/error/nested_errors/reassign_capture_1.err +++ b/tests/error/nested_errors/reassign_capture_1.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Illegal assignment (at $FILE:9:8) + | +7 | +8 | def bar() -> None: +9 | y += 2 + | ^^^^^^ Variable `y` may not be assigned to since `y` is captured + | from an outer scope + | +6 | y = x + 1 + | - `y` defined here -7: -8: def bar() -> None: -9: y += 2 - ^^^^^^ -GuppyError: Variable `y` defined in an outer scope (at 6:4) may not be assigned to +Guppy compilation failed due to 1 previous error diff --git a/tests/error/nested_errors/reassign_capture_2.err b/tests/error/nested_errors/reassign_capture_2.err index 58f44b82..a66be3e6 100644 --- a/tests/error/nested_errors/reassign_capture_2.err +++ b/tests/error/nested_errors/reassign_capture_2.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Illegal assignment (at $FILE:11:8) + | + 9 | if 3 > 2: +10 | z = y +11 | y = 2 + | ^^^^^ Variable `y` may not be assigned to since `y` is captured + | from an outer scope + | + 6 | y = x + 1 + | - `y` defined here -9: if 3 > 2: -10: z = y -11: y = 2 - ^^^^^ -GuppyError: Variable `y` defined in an outer scope (at 6:4) may not be assigned to +Guppy compilation failed due to 1 previous error diff --git a/tests/error/nested_errors/reassign_capture_3.err b/tests/error/nested_errors/reassign_capture_3.err index c226e435..8227921f 100644 --- a/tests/error/nested_errors/reassign_capture_3.err +++ b/tests/error/nested_errors/reassign_capture_3.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Illegal assignment (at $FILE:11:12) + | + 9 | +10 | def baz() -> None: +11 | y += 2 + | ^^^^^^ Variable `y` may not be assigned to since `y` is captured + | from an outer scope + | + 6 | y = x + 1 + | - `y` defined here -9: -10: def baz() -> None: -11: y += 2 - ^^^^^^ -GuppyError: Variable `y` defined in an outer scope (at 6:4) may not be assigned to +Guppy compilation failed due to 1 previous error From bdc6568c63bb9f230783aff179a92c654ff20991 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:08:46 +0000 Subject: [PATCH 11/20] feat: Update linearity checker to use new diagnostics (#601) Closes #541 --- examples/demo.ipynb | 32 +- guppylang/checker/cfg_checker.py | 4 +- guppylang/checker/core.py | 15 + guppylang/checker/errors/__init__.py | 0 guppylang/checker/errors/linearity.py | 259 ++++++++++++ guppylang/checker/expr_checker.py | 5 +- guppylang/checker/func_checker.py | 4 +- guppylang/checker/linearity_checker.py | 372 +++++++++++------- guppylang/checker/stmt_checker.py | 4 +- guppylang/nodes.py | 10 +- .../array_errors/subscript_after_use.err | 17 +- tests/error/array_errors/subscript_drop.err | 19 +- .../array_errors/subscript_non_inout.err | 16 +- .../array_errors/use_after_subscript.err | 17 +- tests/error/comprehension_errors/capture1.err | 15 +- tests/error/comprehension_errors/capture2.err | 16 +- tests/error/comprehension_errors/capture3.err | 15 +- tests/error/comprehension_errors/capture4.err | 15 +- tests/error/comprehension_errors/guarded1.err | 16 +- tests/error/comprehension_errors/guarded2.err | 16 +- tests/error/comprehension_errors/guarded3.err | 16 +- .../error/comprehension_errors/multi_use1.err | 15 +- .../error/comprehension_errors/multi_use2.err | 16 +- .../error/comprehension_errors/multi_use3.err | 18 +- .../error/comprehension_errors/not_used1.err | 13 +- .../error/comprehension_errors/not_used2.err | 13 +- .../pattern_override1.err | 15 +- .../pattern_override2.err | 13 +- .../comprehension_errors/used_twice1.err | 16 +- .../comprehension_errors/used_twice2.err | 16 +- .../comprehension_errors/used_twice3.err | 16 +- tests/error/inout_errors/already_used.err | 17 +- tests/error/inout_errors/drop_after_call.err | 17 +- tests/error/inout_errors/moved.err | 18 +- tests/error/inout_errors/moved_assign.err | 17 +- tests/error/inout_errors/moved_if.err | 18 +- tests/error/inout_errors/moved_out.err | 18 +- tests/error/inout_errors/moved_out_if.err | 18 +- .../nested_call_right_to_left.err | 17 +- .../inout_errors/override_after_call.err | 15 +- tests/error/inout_errors/shadow.err | 15 +- tests/error/inout_errors/shadow_if.err | 15 +- .../error/inout_errors/struct_constructor.err | 18 +- .../error/inout_errors/unused_after_call.err | 15 +- tests/error/linear_errors/branch_use.err | 18 +- .../error/linear_errors/branch_use_field1.err | 18 +- .../error/linear_errors/branch_use_field2.err | 17 +- tests/error/linear_errors/break_unused.err | 18 +- tests/error/linear_errors/call_drop_field.err | 17 +- tests/error/linear_errors/continue_unused.err | 18 +- tests/error/linear_errors/copy_qubit.err | 17 +- tests/error/linear_errors/field_copy1.err | 16 +- tests/error/linear_errors/field_copy2.err | 16 +- tests/error/linear_errors/field_copy3.err | 16 +- .../linear_errors/field_copy_nested1.err | 17 +- .../linear_errors/field_copy_nested2.err | 17 +- .../linear_errors/field_copy_nested3.err | 17 +- .../linear_errors/field_copy_nested4.err | 17 +- tests/error/linear_errors/if_both_unused.err | 15 +- .../linear_errors/if_both_unused_field.err | 15 +- .../linear_errors/if_both_unused_reassign.err | 15 +- .../if_both_unused_reassign_field.err | 14 +- tests/error/linear_errors/method_capture.err | 18 +- tests/error/linear_errors/reassign_unused.err | 14 +- .../linear_errors/reassign_unused_field.err | 14 +- .../linear_errors/reassign_unused_tuple.err | 14 +- tests/error/linear_errors/unused.err | 15 +- tests/error/linear_errors/unused_expr.err | 15 +- tests/error/linear_errors/unused_field1.err | 14 +- tests/error/linear_errors/unused_field2.err | 14 +- tests/error/linear_errors/unused_field3.err | 14 +- .../error/linear_errors/unused_same_block.err | 15 +- tests/error/linear_errors/while_unused.err | 18 +- tests/error/nested_errors/linear_capture.err | 17 +- 74 files changed, 1198 insertions(+), 534 deletions(-) create mode 100644 guppylang/checker/errors/__init__.py create mode 100644 guppylang/checker/errors/linearity.py diff --git a/examples/demo.ipynb b/examples/demo.ipynb index d72e1a54..0f6733fe 100644 --- a/examples/demo.ipynb +++ b/examples/demo.ipynb @@ -306,13 +306,18 @@ "name": "stderr", "output_type": "stream", "text": [ - "Guppy compilation failed. Error in file :6\n", + "Error: Linearity violation (at :6:10)\n", + " | \n", + "4 | @guppy(bad_module)\n", + "5 | def bad(q: qubit @owned) -> qubit:\n", + "6 | cx(q, q)\n", + " | ^ Variable `q` with linear type `qubit` cannot be borrowed\n", + " | ...\n", + " | \n", + "6 | cx(q, q)\n", + " | - since it was already borrowed here\n", "\n", - "4: @guppy(bad_module)\n", - "5: def bad(q: qubit @owned) -> qubit:\n", - "6: cx(q, q)\n", - " ^\n", - "GuppyError: Variable `q` with linear type `qubit` was already used (at 6:7)\n" + "Guppy compilation failed due to 1 previous error\n" ] } ], @@ -350,13 +355,16 @@ "name": "stderr", "output_type": "stream", "text": [ - "Guppy compilation failed. Error in file :7\n", + "Error: Linearity violation (at :7:7)\n", + " | \n", + "5 | def bad(q: qubit @owned) -> qubit:\n", + "6 | tmp = qubit()\n", + "7 | cx(tmp, q)\n", + " | ^^^ Variable `tmp` with linear type `qubit` is leaked\n", + "\n", + "Help: Make sure that `tmp` is consumed or returned to avoid the leak\n", "\n", - "5: def bad(q: qubit @owned) -> qubit:\n", - "6: tmp = qubit()\n", - "7: cx(tmp, q)\n", - " ^^^\n", - "GuppyError: Variable `tmp` with linear type `qubit` is not used on all control-flow paths\n" + "Guppy compilation failed due to 1 previous error\n" ] } ], diff --git a/guppylang/checker/cfg_checker.py b/guppylang/checker/cfg_checker.py index fbd748b9..909d1d5b 100644 --- a/guppylang/checker/cfg_checker.py +++ b/guppylang/checker/cfg_checker.py @@ -60,7 +60,7 @@ def __init__(self, input_tys: list[Type], output_ty: Type) -> None: def check_cfg( - cfg: CFG, inputs: Row[Variable], return_ty: Type, globals: Globals + cfg: CFG, inputs: Row[Variable], return_ty: Type, func_name: str, globals: Globals ) -> CheckedCFG[Place]: """Type checks a control-flow graph. @@ -126,7 +126,7 @@ def check_cfg( # Finally, run the linearity check from guppylang.checker.linearity_checker import check_cfg_linearity - linearity_checked_cfg = check_cfg_linearity(checked_cfg, globals) + linearity_checked_cfg = check_cfg_linearity(checked_cfg, func_name, globals) return linearity_checked_cfg diff --git a/guppylang/checker/core.py b/guppylang/checker/core.py index 41b3c99e..791a5ab1 100644 --- a/guppylang/checker/core.py +++ b/guppylang/checker/core.py @@ -94,6 +94,11 @@ def id(self) -> "Variable.Id": """The unique `PlaceId` identifier for this place.""" return Variable.Id(self.name) + @cached_property + def root(self) -> "Variable": + """The root variable of this place.""" + return self + @property def describe(self) -> str: """A human-readable description of this place for error messages.""" @@ -132,6 +137,11 @@ def id(self) -> "FieldAccess.Id": """The unique `PlaceId` identifier for this place.""" return FieldAccess.Id(self.parent.id, self.field.name) + @cached_property + def root(self) -> "Variable": + """The root variable of this place.""" + return self.parent.root + @property def ty(self) -> Type: """The type of this place.""" @@ -191,6 +201,11 @@ def defined_at(self) -> AstNode | None: """Optional location where this place was last assigned to.""" return self.parent.defined_at + @cached_property + def root(self) -> "Variable": + """The root variable of this place.""" + return self.parent.root + @property def describe(self) -> str: """A human-readable description of this place for error messages.""" diff --git a/guppylang/checker/errors/__init__.py b/guppylang/checker/errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/guppylang/checker/errors/linearity.py b/guppylang/checker/errors/linearity.py new file mode 100644 index 00000000..68638ac9 --- /dev/null +++ b/guppylang/checker/errors/linearity.py @@ -0,0 +1,259 @@ +"""Collection of error messages emitted during linearity checking.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, ClassVar + +from guppylang.diagnostic import Error, Help, Note + +if TYPE_CHECKING: + from guppylang.checker.core import ( + Place, + Variable, + ) + from guppylang.checker.linearity_checker import UseKind + from guppylang.definition.struct import StructField + from guppylang.tys.ty import ( + StructType, + Type, + ) + + +@dataclass(frozen=True) +class AlreadyUsedError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "{place.describe} with linear type `{place.ty}` cannot be {kind.subjunctive} " + "..." + ) + place: Place + kind: UseKind + + @dataclass(frozen=True) + class PrevUse(Note): + span_label: ClassVar[str] = "since it was already {prev_kind.subjunctive} here" + prev_kind: UseKind + + +@dataclass(frozen=True) +class ComprAlreadyUsedError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "{place.describe} with linear type `{place.ty}` would be {kind.subjunctive} " + "multiple times when evaluating this comprehension" + ) + place: Place + kind: UseKind + + @dataclass(frozen=True) + class PrevUse(Note): + span_label: ClassVar[str] = "since it was already {prev_kind.subjunctive} here" + prev_kind: UseKind + + +@dataclass(frozen=True) +class PlaceNotUsedError(Error): + title: ClassVar[str] = "Linearity violation" + place: Place + + @property + def rendered_span_label(self) -> str: + s = f"{self.place.describe} with linear type `{self.place.ty}` " + match self.children: + case [PlaceNotUsedError.Branch(), *_]: + return s + "may be leaked ..." + case _: + return s + "is leaked" + + @dataclass(frozen=True) + class Branch(Note): + span_label: ClassVar[str] = "if this expression is `{truth_value}`" + truth_value: bool + + @dataclass(frozen=True) + class Fix(Help): + message: ClassVar[str] = ( + "Make sure that `{place}` is consumed or returned to avoid the leak" + ) + + +@dataclass(frozen=True) +class UnnamedExprNotUsedError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = "Expression with linear type `{ty}` is leaked" + ty: Type + + @dataclass(frozen=True) + class Fix(Help): + message: ClassVar[str] = "Consider assigning this value to a local variable" + + +@dataclass(frozen=True) +class UnnamedFieldNotUsedError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "Linear field `{field.name}` of expression with type `{struct_ty}` is leaked" + ) + field: StructField + struct_ty: StructType + + @dataclass(frozen=True) + class Fix(Help): + message: ClassVar[str] = ( + "Consider assigning this value to a local variable before accessing the " + "field `{used_field.name}`" + ) + used_field: StructField + + +@dataclass(frozen=True) +class UnnamedSubscriptNotUsedError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "Linear items of expression with type `{container_ty}` are leaked ..." + ) + container_ty: Type + + @dataclass(frozen=True) + class SubscriptHint(Note): + span_label: ClassVar[str] = "since only this subscript is used" + + @dataclass(frozen=True) + class Fix(Help): + message: ClassVar[str] = ( + "Consider assigning this value to a local variable before subscripting it" + ) + + +@dataclass(frozen=True) +class NotOwnedError(Error): + title: ClassVar[str] = "Not owned" + place: Place + kind: UseKind + is_call_arg: bool + func_name: str | None + calling_func_name: str + + @property + def rendered_span_label(self) -> str: + if self.is_call_arg: + f = f"Function `{self.func_name}`" if self.func_name else "Function" + return ( + f"{f} wants to take ownership of this argument, but " + f"`{self.calling_func_name}` doesn't own `{self.place}`" + ) + return ( + f"Cannot {self.kind.indicative} `{self.place}` since " + f"`{self.calling_func_name}` doesn't own it" + ) + + @dataclass(frozen=True) + class MakeOwned(Help): + span_label: ClassVar[str] = ( + "Argument `{place.root.name}` is only borrowed. Consider taking ownership: " + "`{place.root.name}: {place.root.ty} @owned`" + ) + + +@dataclass(frozen=True) +class MoveOutOfSubscriptError(Error): + title: ClassVar[str] = "Subscript {kind.subjunctive}" + span_label: ClassVar[str] = ( + "Cannot {kind.indicative} a subscript of `{parent}` with linear type " + "`{parent.ty}`" + ) + kind: UseKind + parent: Place + + @dataclass(frozen=True) + class Explanation(Note): + message: ClassVar[str] = ( + "Subscripts on linear types are only allowed to be borrowed, not " + "{kind.subjunctive}" + ) + + +@dataclass(frozen=True) +class BorrowShadowedError(Error): + title: ClassVar[str] = "Borrow shadowed" + span_label: ClassVar[str] = "Assignment shadows borrowed argument `{place}`" + place: Place + + @dataclass(frozen=True) + class Rename(Help): + message: ClassVar[str] = "Consider assigning to a different name" + + +@dataclass(frozen=True) +class BorrowSubPlaceUsedError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "Borrowed argument {borrowed_var} cannot be returned to the caller ..." + ) + borrowed_var: Variable + sub_place: Place + + @dataclass(frozen=True) + class PrevUse(Note): + span_label: ClassVar[str] = ( + "since `{sub_place}` with linear type `{sub_place.ty}` was already " + "{kind.subjunctive} here" + ) + kind: UseKind + + @dataclass(frozen=True) + class Fix(Help): + message: ClassVar[str] = ( + "Consider writing a value back into `{sub_place}` before returning" + ) + + +@dataclass(frozen=True) +class DropAfterCallError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "Value with linear type `{ty}` would be leaked after {func} returns" + ) + ty: Type + func_name: str | None + + @property + def func(self) -> str: + return f"`{self.func_name}`" if self.func_name else "the function" + + @dataclass(frozen=True) + class Assign(Help): + message: ClassVar[str] = ( + "Consider assigning the value to a local variable before passing it to " + "{func}" + ) + + +@dataclass(frozen=True) +class LinearCaptureError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "{var.describe} with linear type {var.ty} cannot be used here since `{var}` is " + "captured from an outer scope" + ) + var: Variable + + @dataclass(frozen=True) + class DefinedHere(Note): + span_label: ClassVar[str] = "`{var}` defined here" + + +@dataclass(frozen=True) +class LinearPartialApplyError(Error): + title: ClassVar[str] = "Linearity violation" + span_label: ClassVar[str] = ( + "This expression implicitly constructs a closure that captures a linear value" + ) + + @dataclass(frozen=True) + class Captured(Note): + span_label: ClassVar[str] = ( + "This expression with linear type `{ty}` is implicitly captured" + ) + ty: Type diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index 6bdfe386..b36ca20a 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -586,7 +586,10 @@ def visit_Subscript(self, node: ast.Subscript) -> tuple[ast.expr, Type]: # other indices after this one has been projected out (e.g. `f()[0]` makes # you loose access to all elements besides 0). expr = SubscriptAccessAndDrop( - item=item, item_expr=item_expr, getitem_expr=getitem_expr + item=item, + item_expr=item_expr, + getitem_expr=getitem_expr, + original_expr=node, ) return with_loc(node, expr), result_ty diff --git a/guppylang/checker/func_checker.py b/guppylang/checker/func_checker.py index 7213de4a..896defd2 100644 --- a/guppylang/checker/func_checker.py +++ b/guppylang/checker/func_checker.py @@ -73,7 +73,7 @@ def check_global_func_def( Variable(x, inp.ty, loc, inp.flags) for x, inp, loc in zip(ty.input_names, ty.inputs, args, strict=True) ] - return check_cfg(cfg, inputs, ty.output, globals) + return check_cfg(cfg, inputs, ty.output, func_def.name, globals) def check_nested_func_def( @@ -136,7 +136,7 @@ def check_nested_func_def( # Otherwise, we treat it like a local name inputs.append(Variable(func_def.name, func_def.ty, func_def)) - checked_cfg = check_cfg(cfg, inputs, func_ty.output, globals) + checked_cfg = check_cfg(cfg, inputs, func_ty.output, func_def.name, globals) checked_def = CheckedNestedFunctionDef( def_id, checked_cfg, diff --git a/guppylang/checker/linearity_checker.py b/guppylang/checker/linearity_checker.py index a057b900..17add223 100644 --- a/guppylang/checker/linearity_checker.py +++ b/guppylang/checker/linearity_checker.py @@ -6,7 +6,8 @@ import ast from collections.abc import Generator, Iterator from contextlib import contextmanager -from typing import TypeGuard +from enum import Enum, auto +from typing import TYPE_CHECKING, NamedTuple, TypeGuard from guppylang.ast_util import AstNode, find_nodes, get_type from guppylang.cfg.analysis import LivenessAnalysis @@ -21,10 +22,26 @@ SubscriptAccess, Variable, ) +from guppylang.checker.errors.linearity import ( + AlreadyUsedError, + BorrowShadowedError, + BorrowSubPlaceUsedError, + ComprAlreadyUsedError, + DropAfterCallError, + LinearCaptureError, + LinearPartialApplyError, + MoveOutOfSubscriptError, + NotOwnedError, + PlaceNotUsedError, + UnnamedExprNotUsedError, + UnnamedFieldNotUsedError, + UnnamedSubscriptNotUsedError, +) from guppylang.definition.custom import CustomFunctionDef from guppylang.definition.value import CallableDef from guppylang.error import GuppyError, GuppyTypeError from guppylang.nodes import ( + AnyCall, CheckedNestedFunctionDef, DesugaredGenerator, DesugaredListComp, @@ -44,6 +61,64 @@ StructType, ) +if TYPE_CHECKING: + from guppylang.diagnostic import Error + + +class UseKind(Enum): + """The different ways places can be used.""" + + #: A classical value is copied + COPY = auto() + + #: A value is borrowed when passing it to a function + BORROW = auto() + + #: Ownership of an owned value is transferred by passing it to a function + CONSUME = auto() + + #: Ownership of an owned value is transferred by returning it + RETURN = auto() + + #: An owned value is renamed or stored in a tuple/list + MOVE = auto() + + @property + def indicative(self) -> str: + """Describes a use in an indicative mood. + + For example: "You cannot *consume* this qubit." + """ + return self.name.lower() + + @property + def subjunctive(self) -> str: + """Describes a use in a subjunctive mood. + + For example: "This qubit cannot be *consumed*" + """ + match self: + case UseKind.COPY: + return "copied" + case UseKind.BORROW: + return "borrowed" + case UseKind.CONSUME: + return "consumed" + case UseKind.RETURN: + return "returned" + case UseKind.MOVE: + return "moved" + + +class Use(NamedTuple): + """Records data associated with a use of a place.""" + + #: The AST node corresponding to the use + node: AstNode + + #: The kind of use, i.e. is the value consumed, borrowed, returned, ...? + kind: UseKind + class Scope(Locals[PlaceId, Place]): """Scoped collection of assigned places indexed by their id. @@ -52,33 +127,33 @@ class Scope(Locals[PlaceId, Place]): """ parent_scope: "Scope | None" - used_local: dict[PlaceId, AstNode] - used_parent: dict[PlaceId, AstNode] + used_local: dict[PlaceId, Use] + used_parent: dict[PlaceId, Use] def __init__(self, parent: "Scope | None" = None): self.used_local = {} self.used_parent = {} super().__init__({}, parent) - def used(self, x: PlaceId) -> AstNode | None: + def used(self, x: PlaceId) -> Use | None: """Checks whether a place has already been used.""" if x in self.vars: return self.used_local.get(x, None) assert self.parent_scope is not None return self.parent_scope.used(x) - def use(self, x: PlaceId, node: AstNode) -> None: + def use(self, x: PlaceId, node: AstNode, kind: UseKind) -> None: """Records a use of a place. Works for places in the current scope as well as places in any parent scope. """ if x in self.vars: - self.used_local[x] = node + self.used_local[x] = Use(node, kind) else: assert self.parent_scope is not None assert x in self.parent_scope - self.used_parent[x] = node - self.parent_scope.use(x, node) + self.used_parent[x] = Use(node, kind) + self.parent_scope.use(x, node, kind) def assign(self, place: Place) -> None: """Records an assignment of a place.""" @@ -93,7 +168,8 @@ def stats(self) -> VariableStats[PlaceId]: for x, place in self.vars.items(): assert place.defined_at is not None assigned[x] = place.defined_at - return VariableStats(assigned, self.used_parent) + used = {x: use.node for x, use in self.used_parent.items()} + return VariableStats(assigned, used) class BBLinearityChecker(ast.NodeVisitor): @@ -101,6 +177,7 @@ class BBLinearityChecker(ast.NodeVisitor): scope: Scope stats: VariableStats[PlaceId] + func_name: str func_inputs: dict[PlaceId, Variable] globals: Globals @@ -108,6 +185,7 @@ def check( self, bb: "CheckedBB[Variable]", is_entry: bool, + func_name: str, func_inputs: dict[PlaceId, Variable], globals: Globals, ) -> Scope: @@ -117,6 +195,7 @@ def check( for var in bb.sig.input_row: for place in leaf_places(var): input_scope.assign(place) + self.func_name = func_name self.func_inputs = func_inputs self.globals = globals @@ -139,26 +218,36 @@ def new_scope(self) -> Generator[Scope, None, None]: yield new_scope self.scope = scope - def visit_PlaceNode(self, node: PlaceNode, /, is_inout_arg: bool = False) -> None: + def visit_PlaceNode( + self, + node: PlaceNode, + /, + use_kind: UseKind = UseKind.MOVE, + is_call_arg: AnyCall | None = None, + ) -> None: # Usage of borrowed variables is generally forbidden. The only exception is - # letting them be borrowed by another function call. In that case, our - # `_visit_call_args` helper will set `is_inout_arg=True`. + # letting them be reborrowed by another function call. In that case, our + # `_visit_call_args` helper will set `use_kind=UseKind.BORROW`. + is_inout_arg = use_kind == UseKind.BORROW if is_inout_var(node.place) and not is_inout_arg: - raise GuppyError( - f"{node.place.describe} may not be used in an `@owned` position since " - "it isn't owned. Consider adding a `@owned` annotation to get " - "ownership of the value.", + err: Error = NotOwnedError( node, + node.place, + use_kind, + is_call_arg is not None, + self._call_name(is_call_arg), + self.func_name, ) + arg_span = self.func_inputs[node.place.root.id].defined_at + err.add_sub_diagnostic(NotOwnedError.MakeOwned(arg_span)) + raise GuppyError(err) # Places involving subscripts are handled differently since we ignore everything # after the subscript for the purposes of linearity checking if subscript := contains_subscript(node.place): if not is_inout_arg and subscript.parent.ty.linear: - raise GuppyError( - "Subscripting on expression with linear type " - f"`{subscript.parent.ty}` is not allowed in `@owned` position", - node, - ) + err = MoveOutOfSubscriptError(node, use_kind, subscript.parent) + err.add_sub_diagnostic(MoveOutOfSubscriptError.Explanation(None)) + raise GuppyError(err) self.visit(subscript.item_expr) self.scope.assign(subscript.item) # Visiting the `__getitem__(place.parent, place.item)` call ensures that we @@ -168,14 +257,13 @@ def visit_PlaceNode(self, node: PlaceNode, /, is_inout_arg: bool = False) -> Non else: for place in leaf_places(node.place): x = place.id - if (use := self.scope.used(x)) and place.ty.linear: - raise GuppyError( - f"{place.describe} with linear type `{place.ty}` was already " - "used (at {0})", - node, - [use], + if (prev_use := self.scope.used(x)) and place.ty.linear: + err = AlreadyUsedError(node, place, use_kind) + err.add_sub_diagnostic( + AlreadyUsedError.PrevUse(prev_use.node, prev_use.kind) ) - self.scope.use(x, node) + raise GuppyError(err) + self.scope.use(x, node, use_kind) def visit_Assign(self, node: ast.Assign) -> None: self.visit(node.value) @@ -189,39 +277,50 @@ def visit_Assign(self, node: ast.Assign) -> None: if tgt.place.id in self.func_inputs: entry_place = self.func_inputs[tgt.place.id] if is_inout_var(entry_place): - raise GuppyError( - f"Assignment shadows borrowed argument `{entry_place}`. " - "Consider assigning to a different name.", - tgt.place.defined_at, - ) - - def _visit_call_args(self, func_ty: FunctionType, args: list[ast.expr]) -> None: + err = BorrowShadowedError(tgt.place.defined_at, entry_place) + err.add_sub_diagnostic(BorrowShadowedError.Rename(None)) + raise GuppyError(err) + + def visit_Return(self, node: ast.Return) -> None: + # Intercept returns of places, so we can set the appropriate `use_kind` to get + # nicer error messages + if isinstance(node.value, PlaceNode): + self.visit_PlaceNode(node.value, use_kind=UseKind.RETURN) + elif isinstance(node.value, ast.Tuple): + for elt in node.value.elts: + if isinstance(elt, PlaceNode): + self.visit_PlaceNode(elt, use_kind=UseKind.RETURN) + else: + self.visit(elt) + elif node.value: + self.visit(node.value) + + def _visit_call_args(self, func_ty: FunctionType, call: AnyCall) -> None: """Helper function to check the arguments of a function call. - Populates the `is_inout_arg` kwarg of `visit_PlaceNode` in case some of the + Populates the `use_kind` kwarg of `visit_PlaceNode` in case some of the arguments are places. """ - for inp, arg in zip(func_ty.inputs, args, strict=True): + for inp, arg in zip(func_ty.inputs, call.args, strict=True): if isinstance(arg, PlaceNode): - self.visit_PlaceNode(arg, is_inout_arg=InputFlags.Inout in inp.flags) + use_kind = ( + UseKind.BORROW if InputFlags.Inout in inp.flags else UseKind.CONSUME + ) + self.visit_PlaceNode(arg, use_kind=use_kind, is_call_arg=call) else: self.visit(arg) - def _reassign_inout_args(self, func_ty: FunctionType, args: list[ast.expr]) -> None: + def _reassign_inout_args(self, func_ty: FunctionType, call: AnyCall) -> None: """Helper function to reassign the borrowed arguments after a function call.""" - for inp, arg in zip(func_ty.inputs, args, strict=True): + for inp, arg in zip(func_ty.inputs, call.args, strict=True): if InputFlags.Inout in inp.flags: match arg: case PlaceNode(place=place): self._reassign_single_inout_arg(place, arg) case arg if inp.ty.linear: - raise GuppyError( - f"Borrowed argument with linear type `{inp.ty}` would be " - "dropped after this function call. Consider assigning the " - "expression to a local variable before passing it to the " - "function.", - arg, - ) + err = DropAfterCallError(arg, inp.ty, self._call_name(call)) + err.add_sub_diagnostic(DropAfterCallError.Assign(None)) + raise GuppyError(err) def _reassign_single_inout_arg(self, place: Place, node: ast.expr) -> None: """Helper function to reassign a single borrowed argument after a function @@ -237,6 +336,14 @@ def _reassign_single_inout_arg(self, place: Place, node: ast.expr) -> None: leaf = leaf.replace_defined_at(node) self.scope.assign(leaf) + def _call_name(self, node: AnyCall | None) -> str | None: + """Tries to extract the name of a called function from a call AST node.""" + if isinstance(node, LocalCall): + return node.func.id if isinstance(node.func, ast.Name) else None + elif isinstance(node, GlobalCall): + return self.globals[node.def_id].name + return None + def visit_GlobalCall(self, node: GlobalCall) -> None: func = self.globals[node.def_id] assert isinstance(func, CallableDef) @@ -247,32 +354,29 @@ def visit_GlobalCall(self, node: GlobalCall) -> None: ) else: func_ty = func.ty.instantiate(node.type_args) - self._visit_call_args(func_ty, node.args) - self._reassign_inout_args(func_ty, node.args) + self._visit_call_args(func_ty, node) + self._reassign_inout_args(func_ty, node) def visit_LocalCall(self, node: LocalCall) -> None: func_ty = get_type(node.func) assert isinstance(func_ty, FunctionType) self.visit(node.func) - self._visit_call_args(func_ty, node.args) - self._reassign_inout_args(func_ty, node.args) + self._visit_call_args(func_ty, node) + self._reassign_inout_args(func_ty, node) def visit_TensorCall(self, node: TensorCall) -> None: for arg in node.args: self.visit(arg) - self._reassign_inout_args(node.tensor_ty, node.args) + self._reassign_inout_args(node.tensor_ty, node) def visit_PartialApply(self, node: PartialApply) -> None: self.visit(node.func) for arg in node.args: ty = get_type(arg) if ty.linear: - raise GuppyError( - f"Capturing a value with linear type `{ty}` in a closure is not " - "allowed. Try calling the function directly instead of using it as " - "a higher-order value.", - node, - ) + err = LinearPartialApplyError(node) + err.add_sub_diagnostic(LinearPartialApplyError.Captured(arg, ty)) + raise GuppyError(err) self.visit(arg) def visit_FieldAccessAndDrop(self, node: FieldAccessAndDrop) -> None: @@ -282,11 +386,9 @@ def visit_FieldAccessAndDrop(self, node: FieldAccessAndDrop) -> None: self.visit(node.value) for field in node.struct_ty.fields: if field.name != node.field.name and field.ty.linear: - raise GuppyTypeError( - f"Linear field `{field.name}` of expression with type " - f"`{node.struct_ty}` is not used", - node.value, - ) + err = UnnamedFieldNotUsedError(node.value, field, node.struct_ty) + err.add_sub_diagnostic(UnnamedFieldNotUsedError.Fix(None, node.field)) + raise GuppyError(err) def visit_SubscriptAccessAndDrop(self, node: SubscriptAccessAndDrop) -> None: # A subscript access on a value that is not a place. This means the value can no @@ -294,9 +396,13 @@ def visit_SubscriptAccessAndDrop(self, node: SubscriptAccessAndDrop) -> None: # legal if the items in the container are not linear elem_ty = get_type(node.getitem_expr) if elem_ty.linear: - raise GuppyTypeError( - f"Remaining linear items with type `{elem_ty}` are not used", node + value = node.original_expr.value + err = UnnamedSubscriptNotUsedError(value, get_type(value)) + err.add_sub_diagnostic( + UnnamedSubscriptNotUsedError.SubscriptHint(node.item_expr) ) + err.add_sub_diagnostic(UnnamedSubscriptNotUsedError.Fix(None)) + raise GuppyTypeError(err) self.visit(node.item_expr) self.scope.assign(node.item) self.visit(node.getitem_expr) @@ -306,7 +412,9 @@ def visit_Expr(self, node: ast.Expr) -> None: self.visit(node.value) ty = get_type(node.value) if ty.linear: - raise GuppyTypeError(f"Value with linear type `{ty}` is not used", node) + err = UnnamedExprNotUsedError(node, ty) + err.add_sub_diagnostic(UnnamedExprNotUsedError.Fix(None)) + raise GuppyTypeError(err) def visit_DesugaredListComp(self, node: DesugaredListComp) -> None: self._check_comprehension(node, node.generators) @@ -317,14 +425,11 @@ def visit_CheckedNestedFunctionDef(self, node: CheckedNestedFunctionDef) -> None # TODO: In the future, we could support capturing of non-linear subplaces for var, use in node.captured.values(): if var.ty.linear: - raise GuppyError( - f"{var.describe} with linear type `{var.ty}` may not be used here " - f"because it was defined in an outer scope (at {{0}})", - use, - [var.defined_at], - ) + err = LinearCaptureError(use, var) + err.add_sub_diagnostic(LinearCaptureError.DefinedHere(var.defined_at)) + raise GuppyError(err) for place in leaf_places(var): - self.scope.use(place.id, use) + self.scope.use(place.id, use, UseKind.COPY) self.scope.assign(Variable(node.name, node.ty, node)) def _check_assign_targets(self, targets: list[ast.expr]) -> None: @@ -336,11 +441,9 @@ def _check_assign_targets(self, targets: list[ast.expr]) -> None: # Special error message for shadowing of borrowed vars x = tgt.place.id if x in self.scope.vars and is_inout_var(self.scope[x]): - raise GuppyError( - f"Assignment shadows borrowed argument `{tgt.place}`. " - "Consider assigning to a different name.", - tgt, - ) + err: Error = BorrowShadowedError(tgt, tgt.place) + err.add_sub_diagnostic(BorrowShadowedError.Rename(None)) + raise GuppyError(err) for tgt_place in leaf_places(tgt.place): x = tgt_place.id # Only check for overrides of places locally defined in this BB. Global @@ -348,11 +451,9 @@ def _check_assign_targets(self, targets: list[ast.expr]) -> None: if x in self.scope.vars and x not in self.scope.used_local: place = self.scope[x] if place.ty.linear: - raise GuppyError( - f"{place.describe} with linear type `{place.ty}` is not " - "used", - place.defined_at, - ) + err = PlaceNotUsedError(place.defined_at, place) + err.add_sub_diagnostic(PlaceNotUsedError.Fix(None)) + raise GuppyError(err) self.scope.assign(tgt_place) def _check_comprehension( @@ -392,12 +493,11 @@ def _check_comprehension( for leaf in leaf_places(place): x = leaf.id if not self.scope.used(x) and place.ty.linear: - raise GuppyTypeError( - f"{place.describe} with linear type `{place.ty}` is " - "not used on all control-flow paths of the list " - "comprehension", - place.defined_at, + err = PlaceNotUsedError(place.defined_at, place) + err.add_sub_diagnostic( + PlaceNotUsedError.Branch(first_if, False) ) + raise GuppyTypeError(err) for expr in other_ifs: self.visit(expr) @@ -413,10 +513,7 @@ def _check_comprehension( for leaf in leaf_places(place): x = leaf.id if leaf.ty.linear and not inner_scope.used(x): - raise GuppyTypeError( - f"{leaf.describe} with linear type `{leaf.ty}` is not used", - leaf.defined_at, - ) + raise GuppyTypeError(PlaceNotUsedError(leaf.defined_at, leaf)) # On the other hand, we have to ensure that no linear places from the # outer scope have been used inside the comprehension (they would be used @@ -425,9 +522,7 @@ def _check_comprehension( place = inner_scope[x] if place.ty.linear: raise GuppyTypeError( - f"{place.describe} with linear type `{place.ty}` would be used " - "multiple times when evaluating this comprehension", - use, + ComprAlreadyUsedError(use.node, place, use.kind) ) @@ -459,7 +554,7 @@ def is_inout_var(place: Place) -> TypeGuard[Variable]: def check_cfg_linearity( - cfg: "CheckedCFG[Variable]", globals: Globals + cfg: "CheckedCFG[Variable]", func_name: str, globals: Globals ) -> "CheckedCFG[Place]": """Checks whether a CFG satisfies the linearity requirements. @@ -472,7 +567,11 @@ def check_cfg_linearity( func_inputs: dict[PlaceId, Variable] = {v.id: v for v in cfg.entry_bb.sig.input_row} scopes: dict[BB, Scope] = { bb: bb_checker.check( - bb, is_entry=bb == cfg.entry_bb, func_inputs=func_inputs, globals=globals + bb, + is_entry=bb == cfg.entry_bb, + func_name=func_name, + func_inputs=func_inputs, + globals=globals, ) for bb in cfg.bbs } @@ -482,7 +581,7 @@ def check_cfg_linearity( for var in cfg.entry_bb.sig.input_row: if InputFlags.Inout in var.flags: for leaf in leaf_places(var): - exit_scope.use(leaf.id, InoutReturnSentinel(var=var)) + exit_scope.use(leaf.id, InoutReturnSentinel(var=var), UseKind.RETURN) # Run liveness analysis stats = {bb: scope.stats() for bb, scope in scopes.items()} @@ -505,46 +604,51 @@ def check_cfg_linearity( use = use_scope.used_parent[x] # Special case if this is a use arising from the implicit returning # of a borrowed argument - if isinstance(use, InoutReturnSentinel): - assert isinstance(use.var, Variable) - assert InputFlags.Inout in use.var.flags - raise GuppyError( - f"Borrowed argument `{use.var}` cannot be returned " - f"to the caller since `{place}` is used at {{0}}. " - f"Consider writing a value back into `{place}` before " - "returning.", - use.var.defined_at, - [prev_use], + if isinstance(use.node, InoutReturnSentinel): + assert isinstance(use.node.var, Variable) + assert InputFlags.Inout in use.node.var.flags + err: Error = BorrowSubPlaceUsedError( + use.node.var.defined_at, use.node.var, place + ) + err.add_sub_diagnostic( + BorrowSubPlaceUsedError.PrevUse( + prev_use.node, prev_use.kind + ) ) - raise GuppyError( - f"{place.describe} with linear type `{place.ty}` was " - "already used (at {0})", - use, - [prev_use], + err.add_sub_diagnostic(BorrowSubPlaceUsedError.Fix(None)) + raise GuppyError(err) + err = AlreadyUsedError(use.node, place, use.kind) + err.add_sub_diagnostic( + AlreadyUsedError.PrevUse(prev_use.node, prev_use.kind) ) + raise GuppyError(err) - # On the other hand, unused linear variables *must* be outputted - for place in scope.values(): - for leaf in leaf_places(place): - x = leaf.id - # Some values are just in scope because the type checker determined - # them as live in the first (less precises) dataflow analysis. It - # might be the case that x is actually not live when considering - # the second, more fine-grained, analysis based on places. - if x not in live_before_bb and x not in scope.vars: - continue - used_later = x in live - if leaf.ty.linear and not scope.used(x) and not used_later: - # TODO: This should be "Variable x with linear type ty is not - # used in {bb}". But for this we need a way to associate BBs - # with source locations. - raise GuppyError( - f"{leaf.describe} with linear type `{leaf.ty}` is " - "not used on all control-flow paths", - # Re-lookup defined_at in scope because we might have a - # more precise location - scope[x].defined_at, + # On the other hand, unused linear variables *must* be outputted + for place in scope.values(): + for leaf in leaf_places(place): + x = leaf.id + # Some values are just in scope because the type checker determined + # them as live in the first (less precises) dataflow analysis. It + # might be the case that x is actually not live when considering + # the second, more fine-grained, analysis based on places. + if x not in live_before_bb and x not in scope.vars: + continue + used_later = all(x in live_before[succ] for succ in bb.successors) + if leaf.ty.linear and not scope.used(x) and not used_later: + err = PlaceNotUsedError(scope[x].defined_at, leaf) + # If there are some paths that lead to a consumption, we can give + # a nicer error message by highlighting the branch that leads to + # the leak + if any(x in live_before[succ] for succ in bb.successors): + assert bb.branch_pred is not None + [left_succ, _] = bb.successors + err.add_sub_diagnostic( + PlaceNotUsedError.Branch( + bb.branch_pred, x in live_before[left_succ] + ) ) + err.add_sub_diagnostic(PlaceNotUsedError.Fix(None)) + raise GuppyError(err) def live_places_row(bb: BB, original_row: Row[Variable]) -> Row[Place]: """Construct a row of all places that are live at the start of a given BB. diff --git a/guppylang/checker/stmt_checker.py b/guppylang/checker/stmt_checker.py index b4f8db48..321ec3cf 100644 --- a/guppylang/checker/stmt_checker.py +++ b/guppylang/checker/stmt_checker.py @@ -148,9 +148,7 @@ def visit_AugAssign(self, node: ast.AugAssign) -> ast.stmt: def visit_Expr(self, node: ast.Expr) -> ast.stmt: # An expression statement where the return value is discarded - node.value, ty = self._synth_expr(node.value) - if ty.linear: - raise GuppyTypeError(f"Value with linear type `{ty}` is not used", node) + node.value, _ = self._synth_expr(node.value) return node def visit_Return(self, node: ast.Return) -> ast.stmt: diff --git a/guppylang/nodes.py b/guppylang/nodes.py index ec916433..8b18ea47 100644 --- a/guppylang/nodes.py +++ b/guppylang/nodes.py @@ -70,6 +70,9 @@ class TensorCall(ast.expr): ) +AnyCall = LocalCall | GlobalCall | TensorCall + + class TypeApply(ast.expr): value: ast.expr inst: Inst @@ -116,12 +119,9 @@ class SubscriptAccessAndDrop(ast.expr): item: "Variable" item_expr: ast.expr getitem_expr: ast.expr + original_expr: ast.Subscript - _fields = ( - "item", - "item_expr", - "getitem_expr", - ) + _fields = ("item", "item_expr", "getitem_expr", "original_expr") class MakeIter(ast.expr): diff --git a/tests/error/array_errors/subscript_after_use.err b/tests/error/array_errors/subscript_after_use.err index b234e2c0..458df5bb 100644 --- a/tests/error/array_errors/subscript_after_use.err +++ b/tests/error/array_errors/subscript_after_use.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:19) + | +16 | @guppy(module) +17 | def main(qs: array[qubit, 42] @owned) -> array[qubit, 42]: +18 | return foo(qs, qs[0]) + | ^^ Variable `qs` with linear type `array[qubit, 42]` cannot be + | borrowed ... + | +18 | return foo(qs, qs[0]) + | -- since it was already consumed here -16: @guppy(module) -17: def main(qs: array[qubit, 42] @owned) -> array[qubit, 42]: -18: return foo(qs, qs[0]) - ^^ -GuppyError: Variable `qs` with linear type `array[qubit, 42]` was already used (at 18:15) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/array_errors/subscript_drop.err b/tests/error/array_errors/subscript_drop.err index 448abbbc..c7398e0e 100644 --- a/tests/error/array_errors/subscript_drop.err +++ b/tests/error/array_errors/subscript_drop.err @@ -1,7 +1,14 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:11) + | +16 | @guppy(module) +17 | def main() -> qubit: +18 | return foo()[0] + | ^^^^^ Linear items of expression with type `array[qubit, 10]` are + | leaked ... + | +18 | return foo()[0] + | - since only this subscript is used -16: @guppy(module) -17: def main() -> qubit: -18: return foo()[0] - ^^^^^^^^ -GuppyTypeError: Remaining linear items with type `qubit` are not used +Help: Consider assigning this value to a local variable before subscripting it + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/array_errors/subscript_non_inout.err b/tests/error/array_errors/subscript_non_inout.err index f71f8125..0a35dce5 100644 --- a/tests/error/array_errors/subscript_non_inout.err +++ b/tests/error/array_errors/subscript_non_inout.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Subscript moved (at $FILE:14:8) + | +12 | @guppy(module) +13 | def main(qs: array[qubit, 42]) -> tuple[qubit, array[qubit, 42]]: +14 | q = qs[0] + | ^^^^^ Cannot move a subscript of `qs` with linear type + | `array[qubit, 42]` -12: @guppy(module) -13: def main(qs: array[qubit, 42]) -> tuple[qubit, array[qubit, 42]]: -14: q = qs[0] - ^^^^^ -GuppyError: Subscripting on expression with linear type `array[qubit, 42]` is not allowed in `@owned` position +Note: Subscripts on linear types are only allowed to be borrowed, not moved + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/array_errors/use_after_subscript.err b/tests/error/array_errors/use_after_subscript.err index 8ebdec96..d170f6b2 100644 --- a/tests/error/array_errors/use_after_subscript.err +++ b/tests/error/array_errors/use_after_subscript.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:15) + | +16 | @guppy(module) +17 | def main(qs: array[qubit, 42] @owned) -> array[qubit, 42]: +18 | return foo(qs[0], qs) + | ^^^^^ Variable `qs` with linear type `array[qubit, 42]` cannot be + | borrowed ... + | +18 | return foo(qs[0], qs) + | -- since it was already consumed here -16: @guppy(module) -17: def main(qs: array[qubit, 42] @owned) -> array[qubit, 42]: -18: return foo(qs[0], qs) - ^^^^^ -GuppyError: Variable `qs` with linear type `array[qubit, 42]` was already used (at 18:22) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/capture1.err b/tests/error/comprehension_errors/capture1.err index d5494927..3469936f 100644 --- a/tests/error/comprehension_errors/capture1.err +++ b/tests/error/comprehension_errors/capture1.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:12) + | +11 | @guppy(module) +12 | def foo(xs: list[int], q: qubit @owned) -> list[qubit]: +13 | return [q for x in xs] + | ^ Variable `q` with linear type `qubit` would be moved + | multiple times when evaluating this + | comprehension -11: @guppy(module) -12: def foo(xs: list[int], q: qubit @owned) -> list[qubit]: -13: return [q for x in xs] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` would be used multiple times when evaluating this comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/capture2.err b/tests/error/comprehension_errors/capture2.err index 3d0a6dea..dc9a56eb 100644 --- a/tests/error/comprehension_errors/capture2.err +++ b/tests/error/comprehension_errors/capture2.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:33) + | +16 | @guppy(module) +17 | def foo(xs: list[int], q: qubit @owned) -> list[int]: +18 | return [x for x in xs if bar(q)] + | ^ Variable `q` with linear type `qubit` would be consumed + | multiple times when + | evaluating this + | comprehension -16: @guppy(module) -17: def foo(xs: list[int], q: qubit @owned) -> list[int]: -18: return [x for x in xs if bar(q)] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` would be used multiple times when evaluating this comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/capture3.err b/tests/error/comprehension_errors/capture3.err index fd0d42ad..46b68539 100644 --- a/tests/error/comprehension_errors/capture3.err +++ b/tests/error/comprehension_errors/capture3.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:12) + | +16 | @guppy(module) +17 | def foo(xs: list[int], s: MyStruct @owned) -> list[MyStruct]: +18 | return [s for x in xs] + | ^ Field `s.q` with linear type `qubit` would be moved + | multiple times when evaluating this + | comprehension -16: @guppy(module) -17: def foo(xs: list[int], s: MyStruct @owned) -> list[MyStruct]: -18: return [s for x in xs] - ^ -GuppyTypeError: Field `s.q` with linear type `qubit` would be used multiple times when evaluating this comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/capture4.err b/tests/error/comprehension_errors/capture4.err index f516e7ba..6f80ac54 100644 --- a/tests/error/comprehension_errors/capture4.err +++ b/tests/error/comprehension_errors/capture4.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:12) + | +16 | @guppy(module) +17 | def foo(xs: list[int], s: MyStruct @owned) -> list[qubit]: +18 | return [s.q for x in xs] + | ^^^ Field `s.q` with linear type `qubit` would be moved + | multiple times when evaluating this + | comprehension -16: @guppy(module) -17: def foo(xs: list[int], s: MyStruct @owned) -> list[qubit]: -18: return [s.q for x in xs] - ^^^ -GuppyTypeError: Field `s.q` with linear type `qubit` would be used multiple times when evaluating this comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/guarded1.err b/tests/error/comprehension_errors/guarded1.err index d1b16c92..36f40b3c 100644 --- a/tests/error/comprehension_errors/guarded1.err +++ b/tests/error/comprehension_errors/guarded1.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:21) + | +11 | @guppy(module) +12 | def foo(qs: list[tuple[bool, qubit]] @owned) -> list[qubit]: +13 | return [q for b, q in qs if b] + | ^ Variable `q` with linear type `qubit` may be leaked ... + | +13 | return [q for b, q in qs if b] + | - if this expression is `False` -11: @guppy(module) -12: def foo(qs: list[tuple[bool, qubit]] @owned) -> list[qubit]: -13: return [q for b, q in qs if b] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` is not used on all control-flow paths of the list comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/guarded2.err b/tests/error/comprehension_errors/guarded2.err index 482814e6..26a02d87 100644 --- a/tests/error/comprehension_errors/guarded2.err +++ b/tests/error/comprehension_errors/guarded2.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:22) + | +16 | @guppy(module) +17 | def foo(qs: list[tuple[bool, qubit]] @owned) -> list[int]: +18 | return [42 for b, q in qs if b if bar(q)] + | ^ Variable `q` with linear type `qubit` may be leaked ... + | +18 | return [42 for b, q in qs if b if bar(q)] + | - if this expression is `False` -16: @guppy(module) -17: def foo(qs: list[tuple[bool, qubit]] @owned) -> list[int]: -18: return [42 for b, q in qs if b if bar(q)] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` is not used on all control-flow paths of the list comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/guarded3.err b/tests/error/comprehension_errors/guarded3.err index 54933a77..f7751900 100644 --- a/tests/error/comprehension_errors/guarded3.err +++ b/tests/error/comprehension_errors/guarded3.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:24 +Error: Linearity violation (at $FILE:24:21) + | +22 | @guppy(module) +23 | def foo(qs: list[MyStruct] @owned) -> list[qubit]: +24 | return [s.q2 for s in qs if bar(s.q1)] + | ^ Field `s.q2` with linear type `qubit` may be leaked ... + | +24 | return [s.q2 for s in qs if bar(s.q1)] + | --------- if this expression is `False` -22: @guppy(module) -23: def foo(qs: list[MyStruct] @owned) -> list[qubit]: -24: return [s.q2 for s in qs if bar(s.q1)] - ^ -GuppyTypeError: Field `s.q2` with linear type `qubit` is not used on all control-flow paths of the list comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/multi_use1.err b/tests/error/comprehension_errors/multi_use1.err index d5d69eb2..23fff47f 100644 --- a/tests/error/comprehension_errors/multi_use1.err +++ b/tests/error/comprehension_errors/multi_use1.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:12) + | +11 | @guppy(module) +12 | def foo(qs: list[qubit] @owned, xs: list[int]) -> list[qubit]: +13 | return [q for q in qs for x in xs] + | ^ Variable `q` with linear type `qubit` would be moved + | multiple times when evaluating this + | comprehension -11: @guppy(module) -12: def foo(qs: list[qubit] @owned, xs: list[int]) -> list[qubit]: -13: return [q for q in qs for x in xs] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` would be used multiple times when evaluating this comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/multi_use2.err b/tests/error/comprehension_errors/multi_use2.err index 190b5e1e..abf07cbe 100644 --- a/tests/error/comprehension_errors/multi_use2.err +++ b/tests/error/comprehension_errors/multi_use2.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:35) + | +11 | @guppy(module) +12 | def foo(qs: list[qubit] @owned, xs: list[int]) -> list[qubit]: +13 | return [q for x in xs for q in qs] + | ^^ Variable `qs` with linear type `list[qubit]` would be + | consumed multiple + | times when evaluating + | this comprehension -11: @guppy(module) -12: def foo(qs: list[qubit] @owned, xs: list[int]) -> list[qubit]: -13: return [q for x in xs for q in qs] - ^^ -GuppyTypeError: Variable `qs` with linear type `list[qubit]` would be used multiple times when evaluating this comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/multi_use3.err b/tests/error/comprehension_errors/multi_use3.err index 5f073292..243cf92f 100644 --- a/tests/error/comprehension_errors/multi_use3.err +++ b/tests/error/comprehension_errors/multi_use3.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:45) + | +16 | @guppy(module) +17 | def foo(qs: list[qubit] @owned, xs: list[int]) -> list[int]: +18 | return [x for q in qs for x in xs if bar(q)] + | ^ Variable `q` with linear type `qubit` would be consumed + | multiple + | times when + | evaluating + | this + | comprehension -16: @guppy(module) -17: def foo(qs: list[qubit] @owned, xs: list[int]) -> list[int]: -18: return [x for q in qs for x in xs if bar(q)] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` would be used multiple times when evaluating this comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/not_used1.err b/tests/error/comprehension_errors/not_used1.err index 3eb2c80f..1eb44896 100644 --- a/tests/error/comprehension_errors/not_used1.err +++ b/tests/error/comprehension_errors/not_used1.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:19) + | +11 | @guppy(module) +12 | def foo(qs: list[qubit] @owned) -> list[int]: +13 | return [42 for q in qs] + | ^ Variable `q` with linear type `qubit` is leaked -11: @guppy(module) -12: def foo(qs: list[qubit] @owned) -> list[int]: -13: return [42 for q in qs] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` is not used +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/not_used2.err b/tests/error/comprehension_errors/not_used2.err index e7ffd446..a0e69fed 100644 --- a/tests/error/comprehension_errors/not_used2.err +++ b/tests/error/comprehension_errors/not_used2.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:19 +Error: Linearity violation (at $FILE:19:21) + | +17 | @guppy(module) +18 | def foo(ss: list[MyStruct] @owned) -> list[qubit]: +19 | return [s.q1 for s in ss] + | ^ Field `s.q2` with linear type `qubit` is leaked -17: @guppy(module) -18: def foo(ss: list[MyStruct] @owned) -> list[qubit]: -19: return [s.q1 for s in ss] - ^ -GuppyTypeError: Field `s.q2` with linear type `qubit` is not used +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/pattern_override1.err b/tests/error/comprehension_errors/pattern_override1.err index 58a8b594..3e110617 100644 --- a/tests/error/comprehension_errors/pattern_override1.err +++ b/tests/error/comprehension_errors/pattern_override1.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:18) + | +11 | @guppy(module) +12 | def foo(qs: list[tuple[qubit, qubit]] @owned) -> list[qubit]: +13 | return [q for q, q in qs] + | ^ Variable `q` with linear type `qubit` is leaked -11: @guppy(module) -12: def foo(qs: list[tuple[qubit, qubit]] @owned) -> list[qubit]: -13: return [q for q, q in qs] - ^ -GuppyError: Variable `q` with linear type `qubit` is not used +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/pattern_override2.err b/tests/error/comprehension_errors/pattern_override2.err index acf2e0cd..15f4f509 100644 --- a/tests/error/comprehension_errors/pattern_override2.err +++ b/tests/error/comprehension_errors/pattern_override2.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:18) + | +11 | @guppy(module) +12 | def foo(qs: list[qubit] @owned, xs: list[int]) -> list[int]: +13 | return [q for q in qs for q in xs] + | ^ Variable `q` with linear type `qubit` is leaked -11: @guppy(module) -12: def foo(qs: list[qubit] @owned, xs: list[int]) -> list[int]: -13: return [q for q in qs for q in xs] - ^ -GuppyTypeError: Variable `q` with linear type `qubit` is not used +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/used_twice1.err b/tests/error/comprehension_errors/used_twice1.err index 142b7f85..6cb18ca1 100644 --- a/tests/error/comprehension_errors/used_twice1.err +++ b/tests/error/comprehension_errors/used_twice1.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:16) + | +11 | @guppy(module) +12 | def foo(qs: list[qubit] @owned) -> list[tuple[qubit, qubit]]: +13 | return [(q, q) for q in qs] + | ^ Variable `q` with linear type `qubit` cannot be moved ... + | +13 | return [(q, q) for q in qs] + | - since it was already moved here -11: @guppy(module) -12: def foo(qs: list[qubit] @owned) -> list[tuple[qubit, qubit]]: -13: return [(q, q) for q in qs] - ^ -GuppyError: Variable `q` with linear type `qubit` was already used (at 13:13) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/used_twice2.err b/tests/error/comprehension_errors/used_twice2.err index 7d8d0237..d58543db 100644 --- a/tests/error/comprehension_errors/used_twice2.err +++ b/tests/error/comprehension_errors/used_twice2.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:12) + | +16 | @guppy(module) +17 | def foo(qs: list[qubit] @owned) -> list[qubit]: +18 | return [q for q in qs if bar(q)] + | ^ Variable `q` with linear type `qubit` cannot be moved ... + | +18 | return [q for q in qs if bar(q)] + | - since it was already consumed here -16: @guppy(module) -17: def foo(qs: list[qubit] @owned) -> list[qubit]: -18: return [q for q in qs if bar(q)] - ^ -GuppyError: Variable `q` with linear type `qubit` was already used (at 18:33) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/used_twice3.err b/tests/error/comprehension_errors/used_twice3.err index 0cc3f964..0e253e52 100644 --- a/tests/error/comprehension_errors/used_twice3.err +++ b/tests/error/comprehension_errors/used_twice3.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:12) + | +16 | @guppy(module) +17 | def foo(qs: list[qubit] @owned) -> list[qubit]: +18 | return [q for q in qs for x in bar(q)] + | ^ Variable `q` with linear type `qubit` cannot be moved ... + | +18 | return [q for q in qs for x in bar(q)] + | - since it was already consumed here -16: @guppy(module) -17: def foo(qs: list[qubit] @owned) -> list[qubit]: -18: return [q for q in qs for x in bar(q)] - ^ -GuppyError: Variable `q` with linear type `qubit` was already used (at 18:39) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/already_used.err b/tests/error/inout_errors/already_used.err index 22f6506b..470df68c 100644 --- a/tests/error/inout_errors/already_used.err +++ b/tests/error/inout_errors/already_used.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:21 +Error: Linearity violation (at $FILE:21:8) + | +19 | def test(q: qubit @owned) -> None: +20 | use(q) +21 | foo(q) + | ^ Variable `q` with linear type `qubit` cannot be borrowed + | ... + | +20 | use(q) + | - since it was already consumed here -19: def test(q: qubit @owned) -> None: -20: use(q) -21: foo(q) - ^ -GuppyError: Variable `q` with linear type `qubit` was already used (at 20:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/drop_after_call.err b/tests/error/inout_errors/drop_after_call.err index 9a3c6aa7..ad4ad6ed 100644 --- a/tests/error/inout_errors/drop_after_call.err +++ b/tests/error/inout_errors/drop_after_call.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:15 +Error: Linearity violation (at $FILE:15:7) + | +13 | @guppy(module) +14 | def test() -> None: +15 | foo(qubit()) + | ^^^^^^^ Value with linear type `qubit` would be leaked after `foo` + | returns -13: @guppy(module) -14: def test() -> None: -15: foo(qubit()) - ^^^^^^^ -GuppyError: Borrowed argument with linear type `qubit` would be dropped after this function call. Consider assigning the expression to a local variable before passing it to the function. +Help: Consider assigning the value to a local variable before passing it to +`foo` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/moved.err b/tests/error/inout_errors/moved.err index 8c22bff4..962c4484 100644 --- a/tests/error/inout_errors/moved.err +++ b/tests/error/inout_errors/moved.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:21 +Error: Not owned (at $FILE:21:8) + | +19 | def test(q: qubit) -> None: +20 | foo(q) +21 | use(q) + | ^ Function `use` wants to take ownership of this argument, + | but `test` doesn't own `q` + | +19 | def test(q: qubit) -> None: + | -------- Argument `q` is only borrowed. Consider taking ownership: + | `q: qubit @owned` -19: def test(q: qubit) -> None: -20: foo(q) -21: use(q) - ^ -GuppyError: Variable `q` may not be used in an `@owned` position since it isn't owned. Consider adding a `@owned` annotation to get ownership of the value. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/moved_assign.err b/tests/error/inout_errors/moved_assign.err index 1ef88bf8..f2c78aa0 100644 --- a/tests/error/inout_errors/moved_assign.err +++ b/tests/error/inout_errors/moved_assign.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Not owned (at $FILE:11:8) + | + 9 | @guppy(module) +10 | def test(q: qubit) -> qubit: +11 | r = q + | ^ Cannot move `q` since `test` doesn't own it + | +10 | def test(q: qubit) -> qubit: + | -------- Argument `q` is only borrowed. Consider taking ownership: + | `q: qubit @owned` -9: @guppy(module) -10: def test(q: qubit) -> qubit: -11: r = q - ^ -GuppyError: Variable `q` may not be used in an `@owned` position since it isn't owned. Consider adding a `@owned` annotation to get ownership of the value. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/moved_if.err b/tests/error/inout_errors/moved_if.err index 42c6c8c4..007da48e 100644 --- a/tests/error/inout_errors/moved_if.err +++ b/tests/error/inout_errors/moved_if.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:17 +Error: Not owned (at $FILE:17:12) + | +15 | def test(q: qubit, b: bool) -> None: +16 | if b: +17 | use(q) + | ^ Function `use` wants to take ownership of this argument, + | but `test` doesn't own `q` + | +15 | def test(q: qubit, b: bool) -> None: + | -------- Argument `q` is only borrowed. Consider taking ownership: + | `q: qubit @owned` -15: def test(q: qubit, b: bool) -> None: -16: if b: -17: use(q) - ^ -GuppyError: Variable `q` may not be used in an `@owned` position since it isn't owned. Consider adding a `@owned` annotation to get ownership of the value. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/moved_out.err b/tests/error/inout_errors/moved_out.err index c376dd5b..0aff5a67 100644 --- a/tests/error/inout_errors/moved_out.err +++ b/tests/error/inout_errors/moved_out.err @@ -1,6 +1,14 @@ -Guppy compilation failed. Error in file $FILE:20 +Error: Linearity violation (at $FILE:20:9) + | +18 | +19 | @guppy(module) +20 | def test(s: MyStruct) -> None: + | ^^^^^^^^^^^ Borrowed argument s cannot be returned to the caller ... + | +21 | use(s.q) + | --- since `s.q` with linear type `qubit` was already consumed + | here -18: @guppy(module) -19: def test(s: MyStruct) -> None: - ^^^^^^^^^^^ -GuppyError: Borrowed argument `s` cannot be returned to the caller since `s.q` is used at 21:8. Consider writing a value back into `s.q` before returning. +Help: Consider writing a value back into `s.q` before returning + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/moved_out_if.err b/tests/error/inout_errors/moved_out_if.err index e8fe017b..202f2d35 100644 --- a/tests/error/inout_errors/moved_out_if.err +++ b/tests/error/inout_errors/moved_out_if.err @@ -1,6 +1,14 @@ -Guppy compilation failed. Error in file $FILE:20 +Error: Linearity violation (at $FILE:20:9) + | +18 | +19 | @guppy(module) +20 | def test(s: MyStruct, b: bool) -> None: + | ^^^^^^^^^^^ Borrowed argument s cannot be returned to the caller ... + | +22 | use(s.q) + | --- since `s.q` with linear type `qubit` was already consumed + | here -18: @guppy(module) -19: def test(s: MyStruct, b: bool) -> None: - ^^^^^^^^^^^ -GuppyError: Borrowed argument `s` cannot be returned to the caller since `s.q` is used at 22:12. Consider writing a value back into `s.q` before returning. +Help: Consider writing a value back into `s.q` before returning + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/nested_call_right_to_left.err b/tests/error/inout_errors/nested_call_right_to_left.err index 1d58cccc..2a64baf8 100644 --- a/tests/error/inout_errors/nested_call_right_to_left.err +++ b/tests/error/inout_errors/nested_call_right_to_left.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:17 +Error: Linearity violation (at $FILE:17:22) + | +15 | def test(q: qubit @owned) -> tuple[int, qubit]: +16 | # This doesn't work since arguments are evaluated from left to right +17 | return foo(q, foo(q, foo(q, 0))), q + | ^ Variable `q` with linear type `qubit` cannot be borrowed + | ... + | +17 | return foo(q, foo(q, foo(q, 0))), q + | - since it was already borrowed here -15: def test(q: qubit @owned) -> tuple[int, qubit]: -16: # This doesn't work since arguments are evaluated from left to right -17: return foo(q, foo(q, foo(q, 0))), q - ^ -GuppyError: Variable `q` with linear type `qubit` was already used (at 17:15) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/override_after_call.err b/tests/error/inout_errors/override_after_call.err index 710bee33..3b7b5290 100644 --- a/tests/error/inout_errors/override_after_call.err +++ b/tests/error/inout_errors/override_after_call.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:16 +Error: Linearity violation (at $FILE:16:13) + | +14 | @guppy(module) +15 | def test(q1: qubit @owned, q2: qubit @owned) -> tuple[qubit, qubit]: +16 | q1 = foo(q1, q2) + | ^^ Variable `q1` with linear type `qubit` is leaked -14: @guppy(module) -15: def test(q1: qubit @owned, q2: qubit @owned) -> tuple[qubit, qubit]: -16: q1 = foo(q1, q2) - ^^ -GuppyError: Variable `q1` with linear type `qubit` is not used +Help: Make sure that `q1` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/shadow.err b/tests/error/inout_errors/shadow.err index c94a0743..74cb9032 100644 --- a/tests/error/inout_errors/shadow.err +++ b/tests/error/inout_errors/shadow.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Borrow shadowed (at $FILE:12:4) + | +10 | @guppy(module) +11 | def test(q: qubit) -> None: +12 | q = qubit() + | ^ Assignment shadows borrowed argument `q` -10: @guppy(module) -11: def test(q: qubit) -> None: -12: q = qubit() - ^ -GuppyError: Assignment shadows borrowed argument `q`. Consider assigning to a different name. +Help: Consider assigning to a different name + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/shadow_if.err b/tests/error/inout_errors/shadow_if.err index 7bb390a5..0dac24da 100644 --- a/tests/error/inout_errors/shadow_if.err +++ b/tests/error/inout_errors/shadow_if.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Borrow shadowed (at $FILE:13:8) + | +11 | def test(q: qubit, b: bool) -> None: +12 | if b: +13 | q = qubit() + | ^ Assignment shadows borrowed argument `q` -11: def test(q: qubit, b: bool) -> None: -12: if b: -13: q = qubit() - ^ -GuppyError: Assignment shadows borrowed argument `q`. Consider assigning to a different name. +Help: Consider assigning to a different name + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/struct_constructor.err b/tests/error/inout_errors/struct_constructor.err index 9daecd30..c6d7a7fc 100644 --- a/tests/error/inout_errors/struct_constructor.err +++ b/tests/error/inout_errors/struct_constructor.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:15 +Error: Not owned (at $FILE:15:20) + | +13 | @guppy(module) +14 | def test(q: qubit) -> MyStruct: +15 | return MyStruct(q) + | ^ Function `__new__` wants to take ownership of this + | argument, but `test` doesn't own `q` + | +14 | def test(q: qubit) -> MyStruct: + | -------- Argument `q` is only borrowed. Consider taking ownership: + | `q: qubit @owned` -13: @guppy(module) -14: def test(q: qubit) -> MyStruct: -15: return MyStruct(q) - ^ -GuppyError: Variable `q` may not be used in an `@owned` position since it isn't owned. Consider adding a `@owned` annotation to get ownership of the value. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/unused_after_call.err b/tests/error/inout_errors/unused_after_call.err index 04e57abe..e2bc014c 100644 --- a/tests/error/inout_errors/unused_after_call.err +++ b/tests/error/inout_errors/unused_after_call.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:16 +Error: Linearity violation (at $FILE:16:7) + | +14 | @guppy(module) +15 | def test(q: qubit @owned) -> None: +16 | foo(q) + | ^ Variable `q` with linear type `qubit` is leaked -14: @guppy(module) -15: def test(q: qubit @owned) -> None: -16: foo(q) - ^ -GuppyError: Variable `q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/branch_use.err b/tests/error/linear_errors/branch_use.err index d37d2b32..18bf91fa 100644 --- a/tests/error/linear_errors/branch_use.err +++ b/tests/error/linear_errors/branch_use.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:24 +Error: Linearity violation (at $FILE:24:4) + | +22 | @guppy(module) +23 | def foo(b: bool) -> bool: +24 | q = new_qubit() + | ^ Variable `q` with linear type `qubit` may be leaked ... + | +25 | if b: + | - if this expression is `False` -22: @guppy(module) -23: def foo(b: bool) -> bool: -24: q = new_qubit() - ^ -GuppyError: Variable `q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/branch_use_field1.err b/tests/error/linear_errors/branch_use_field1.err index 4e29eb90..e2247936 100644 --- a/tests/error/linear_errors/branch_use_field1.err +++ b/tests/error/linear_errors/branch_use_field1.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:4) + | +16 | @guppy(module) +17 | def foo(b: bool) -> bool: +18 | s = MyStruct(qubit()) + | ^ Field `s.q` with linear type `qubit` may be leaked ... + | +19 | if b: + | - if this expression is `False` -16: @guppy(module) -17: def foo(b: bool) -> bool: -18: s = MyStruct(qubit()) - ^ -GuppyError: Field `s.q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `s.q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/branch_use_field2.err b/tests/error/linear_errors/branch_use_field2.err index e855382f..985f1de1 100644 --- a/tests/error/linear_errors/branch_use_field2.err +++ b/tests/error/linear_errors/branch_use_field2.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:22 +Error: Linearity violation (at $FILE:22:11) + | +20 | if b: +21 | measure(s.q2) +22 | return s + | ^ Field `s.q2` with linear type `qubit` cannot be returned + | ... + | +21 | measure(s.q2) + | ---- since it was already consumed here -20: if b: -21: measure(s.q2) -22: return s - ^ -GuppyError: Field `s.q2` with linear type `qubit` was already used (at 21:16) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/break_unused.err b/tests/error/linear_errors/break_unused.err index d3bbe74e..49abb207 100644 --- a/tests/error/linear_errors/break_unused.err +++ b/tests/error/linear_errors/break_unused.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:26 +Error: Linearity violation (at $FILE:26:8) + | +24 | b = False +25 | while True: +26 | q = new_qubit() + | ^ Variable `q` with linear type `qubit` may be leaked ... + | +27 | if i == 0: + | ------ if this expression is `True` -24: b = False -25: while True: -26: q = new_qubit() - ^ -GuppyError: Variable `q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/call_drop_field.err b/tests/error/linear_errors/call_drop_field.err index 6d5520aa..5d2f10fa 100644 --- a/tests/error/linear_errors/call_drop_field.err +++ b/tests/error/linear_errors/call_drop_field.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:24 +Error: Linearity violation (at $FILE:24:11) + | +22 | @guppy(module) +23 | def bar() -> qubit: +24 | return foo().q1 + | ^^^^^ Linear field `q2` of expression with type `MyStruct` is + | leaked -22: @guppy(module) -23: def bar() -> qubit: -24: return foo().q1 - ^^^^^ -GuppyTypeError: Linear field `q2` of expression with type `MyStruct` is not used +Help: Consider assigning this value to a local variable before accessing the +field `q1` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/continue_unused.err b/tests/error/linear_errors/continue_unused.err index ca69e2ec..311fac58 100644 --- a/tests/error/linear_errors/continue_unused.err +++ b/tests/error/linear_errors/continue_unused.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:26 +Error: Linearity violation (at $FILE:26:8) + | +24 | b = False +25 | while i > 0: +26 | q = new_qubit() + | ^ Variable `q` with linear type `qubit` may be leaked ... + | +27 | if i % 10 == 0: + | ----------- if this expression is `True` -24: b = False -25: while i > 0: -26: q = new_qubit() - ^ -GuppyError: Variable `q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/copy_qubit.err b/tests/error/linear_errors/copy_qubit.err index 19fb3417..ef7bd8e5 100644 --- a/tests/error/linear_errors/copy_qubit.err +++ b/tests/error/linear_errors/copy_qubit.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Linearity violation (at $FILE:14:14) + | +12 | @guppy(module) +13 | def foo(q: qubit @owned) -> tuple[qubit, qubit]: +14 | return q, q + | ^ Variable `q` with linear type `qubit` cannot be returned + | ... + | +14 | return q, q + | - since it was already returned here -12: @guppy(module) -13: def foo(q: qubit @owned) -> tuple[qubit, qubit]: -14: return q, q - ^ -GuppyError: Variable `q` with linear type `qubit` was already used (at 14:11) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/field_copy1.err b/tests/error/linear_errors/field_copy1.err index e19ceaf9..d3da9d5d 100644 --- a/tests/error/linear_errors/field_copy1.err +++ b/tests/error/linear_errors/field_copy1.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:20 +Error: Linearity violation (at $FILE:20:11) + | +18 | def foo(s: MyStruct @owned) -> tuple[qubit, qubit]: +19 | t = s +20 | return s.q, t.q + | ^^^ Field `s.q` with linear type `qubit` cannot be returned ... + | +19 | t = s + | - since it was already moved here -18: def foo(s: MyStruct @owned) -> tuple[qubit, qubit]: -19: t = s -20: return s.q, t.q - ^^^ -GuppyError: Field `s.q` with linear type `qubit` was already used (at 19:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/field_copy2.err b/tests/error/linear_errors/field_copy2.err index 5980884e..33d2ee5a 100644 --- a/tests/error/linear_errors/field_copy2.err +++ b/tests/error/linear_errors/field_copy2.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:14) + | +16 | @guppy(module) +17 | def foo(s: MyStruct @owned) -> tuple[MyStruct, qubit]: +18 | return s, s.q + | ^^^ Field `s.q` with linear type `qubit` cannot be returned ... + | +18 | return s, s.q + | - since it was already returned here -16: @guppy(module) -17: def foo(s: MyStruct @owned) -> tuple[MyStruct, qubit]: -18: return s, s.q - ^^^ -GuppyError: Field `s.q` with linear type `qubit` was already used (at 18:11) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/field_copy3.err b/tests/error/linear_errors/field_copy3.err index 43ee1108..cadfc23d 100644 --- a/tests/error/linear_errors/field_copy3.err +++ b/tests/error/linear_errors/field_copy3.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:16) + | +16 | @guppy(module) +17 | def foo(s: MyStruct @owned) -> tuple[qubit, MyStruct]: +18 | return s.q, s + | ^ Field `s.q` with linear type `qubit` cannot be returned ... + | +18 | return s.q, s + | --- since it was already returned here -16: @guppy(module) -17: def foo(s: MyStruct @owned) -> tuple[qubit, MyStruct]: -18: return s.q, s - ^ -GuppyError: Field `s.q` with linear type `qubit` was already used (at 18:11) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/field_copy_nested1.err b/tests/error/linear_errors/field_copy_nested1.err index 29998ac9..17df3d4d 100644 --- a/tests/error/linear_errors/field_copy_nested1.err +++ b/tests/error/linear_errors/field_copy_nested1.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:27 +Error: Linearity violation (at $FILE:27:11) + | +25 | measure(s.q) +26 | s.q = s.x.q +27 | return s + | ^ Field `s.x.q` with linear type `qubit` cannot be returned + | ... + | +26 | s.q = s.x.q + | ----- since it was already moved here -25: measure(s.q) -26: s.q = s.x.q -27: return s - ^ -GuppyError: Field `s.x.q` with linear type `qubit` was already used (at 26:10) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/field_copy_nested2.err b/tests/error/linear_errors/field_copy_nested2.err index 0a87bbea..622c5929 100644 --- a/tests/error/linear_errors/field_copy_nested2.err +++ b/tests/error/linear_errors/field_copy_nested2.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:26 +Error: Linearity violation (at $FILE:26:21) + | +24 | def foo(s: MyStruct1 @owned) -> MyStruct1: +25 | measure(s.x.q1) +26 | return MyStruct1(s.x) + | ^^^ Field `s.x.q1` with linear type `qubit` cannot be consumed + | ... + | +25 | measure(s.x.q1) + | ------ since it was already consumed here -24: def foo(s: MyStruct1 @owned) -> MyStruct1: -25: measure(s.x.q1) -26: return MyStruct1(s.x) - ^^^ -GuppyError: Field `s.x.q1` with linear type `qubit` was already used (at 25:12) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/field_copy_nested3.err b/tests/error/linear_errors/field_copy_nested3.err index 27fa435b..8143d2a6 100644 --- a/tests/error/linear_errors/field_copy_nested3.err +++ b/tests/error/linear_errors/field_copy_nested3.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:29 +Error: Linearity violation (at $FILE:29:11) + | +27 | def foo(s: MyStruct1 @owned) -> qubit: +28 | use(s.x) +29 | return s.x.q + | ^^^^^ Field `s.x.q` with linear type `qubit` cannot be returned + | ... + | +28 | use(s.x) + | --- since it was already consumed here -27: def foo(s: MyStruct1 @owned) -> qubit: -28: use(s.x) -29: return s.x.q - ^^^^^ -GuppyError: Field `s.x.q` with linear type `qubit` was already used (at 28:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/field_copy_nested4.err b/tests/error/linear_errors/field_copy_nested4.err index 8f2f86f2..04949169 100644 --- a/tests/error/linear_errors/field_copy_nested4.err +++ b/tests/error/linear_errors/field_copy_nested4.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:29 +Error: Linearity violation (at $FILE:29:11) + | +27 | def foo(s: MyStruct1 @owned) -> MyStruct1: +28 | use(s) +29 | return s + | ^ Field `s.x.q` with linear type `qubit` cannot be returned + | ... + | +28 | use(s) + | - since it was already consumed here -27: def foo(s: MyStruct1 @owned) -> MyStruct1: -28: use(s) -29: return s - ^ -GuppyError: Field `s.x.q` with linear type `qubit` was already used (at 28:8) +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/if_both_unused.err b/tests/error/linear_errors/if_both_unused.err index e0291646..da4794d6 100644 --- a/tests/error/linear_errors/if_both_unused.err +++ b/tests/error/linear_errors/if_both_unused.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:19 +Error: Linearity violation (at $FILE:19:8) + | +17 | def foo(b: bool) -> int: +18 | if b: +19 | q = new_qubit() + | ^ Variable `q` with linear type `qubit` is leaked -17: def foo(b: bool) -> int: -18: if b: -19: q = new_qubit() - ^ -GuppyError: Variable `q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/if_both_unused_field.err b/tests/error/linear_errors/if_both_unused_field.err index 8fd1b028..cd11e971 100644 --- a/tests/error/linear_errors/if_both_unused_field.err +++ b/tests/error/linear_errors/if_both_unused_field.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:19 +Error: Linearity violation (at $FILE:19:8) + | +17 | def foo(b: bool) -> int: +18 | if b: +19 | s = MyStruct(qubit()) + | ^ Field `s.q` with linear type `qubit` is leaked -17: def foo(b: bool) -> int: -18: if b: -19: s = MyStruct(qubit()) - ^ -GuppyError: Field `s.q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `s.q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/if_both_unused_reassign.err b/tests/error/linear_errors/if_both_unused_reassign.err index c96b2c83..911f98dc 100644 --- a/tests/error/linear_errors/if_both_unused_reassign.err +++ b/tests/error/linear_errors/if_both_unused_reassign.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:19 +Error: Linearity violation (at $FILE:19:8) + | +17 | def foo(b: bool) -> qubit: +18 | if b: +19 | q = new_qubit() + | ^ Variable `q` with linear type `qubit` is leaked -17: def foo(b: bool) -> qubit: -18: if b: -19: q = new_qubit() - ^ -GuppyError: Variable `q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/if_both_unused_reassign_field.err b/tests/error/linear_errors/if_both_unused_reassign_field.err index 17fc50a5..bd53e9e3 100644 --- a/tests/error/linear_errors/if_both_unused_reassign_field.err +++ b/tests/error/linear_errors/if_both_unused_reassign_field.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:17) + | +16 | +17 | @guppy(module) +18 | def foo(b: bool, s: MyStruct @owned) -> MyStruct: + | ^^^^^^^^^^^^^^^^^^ Field `s.q` with linear type `qubit` is leaked -16: @guppy(module) -17: def foo(b: bool, s: MyStruct @owned) -> MyStruct: - ^^^^^^^^^^^^^^^^^^ -GuppyError: Field `s.q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `s.q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/method_capture.err b/tests/error/linear_errors/method_capture.err index 30a4c538..378d7f2e 100644 --- a/tests/error/linear_errors/method_capture.err +++ b/tests/error/linear_errors/method_capture.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:23 +Error: Linearity violation (at $FILE:23:8) + | +21 | @guppy(module) +22 | def foo(s: Struct @owned) -> Struct: +23 | f = s.foo + | ^^^^^ This expression implicitly constructs a closure that + | captures a linear value + | +23 | f = s.foo + | - This expression with linear type `Struct` is implicitly + | captured -21: @guppy(module) -22: def foo(s: Struct @owned) -> Struct: -23: f = s.foo - ^^^^^ -GuppyError: Capturing a value with linear type `Struct` in a closure is not allowed. Try calling the function directly instead of using it as a higher-order value. +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/reassign_unused.err b/tests/error/linear_errors/reassign_unused.err index b1d87e94..b352370b 100644 --- a/tests/error/linear_errors/reassign_unused.err +++ b/tests/error/linear_errors/reassign_unused.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:8) + | +16 | +17 | @guppy(module) +18 | def foo(q: qubit @owned) -> qubit: + | ^^^^^^^^^^^^^^^ Variable `q` with linear type `qubit` is leaked -16: @guppy(module) -17: def foo(q: qubit @owned) -> qubit: - ^^^^^^^^^^^^^^^ -GuppyError: Variable `q` with linear type `qubit` is not used +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/reassign_unused_field.err b/tests/error/linear_errors/reassign_unused_field.err index 367b703b..3f3ffb91 100644 --- a/tests/error/linear_errors/reassign_unused_field.err +++ b/tests/error/linear_errors/reassign_unused_field.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:8) + | +16 | +17 | @guppy(module) +18 | def foo(s: MyStruct @owned) -> MyStruct: + | ^^^^^^^^^^^^^^^^^^ Field `s.q` with linear type `qubit` is leaked -16: @guppy(module) -17: def foo(s: MyStruct @owned) -> MyStruct: - ^^^^^^^^^^^^^^^^^^ -GuppyError: Field `s.q` with linear type `qubit` is not used +Help: Make sure that `s.q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/reassign_unused_tuple.err b/tests/error/linear_errors/reassign_unused_tuple.err index 67843132..f58f3b21 100644 --- a/tests/error/linear_errors/reassign_unused_tuple.err +++ b/tests/error/linear_errors/reassign_unused_tuple.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:18 +Error: Linearity violation (at $FILE:18:8) + | +16 | +17 | @guppy(module) +18 | def foo(q: qubit @owned) -> tuple[qubit, qubit]: + | ^^^^^^^^^^^^^^^ Variable `q` with linear type `qubit` is leaked -16: @guppy(module) -17: def foo(q: qubit @owned) -> tuple[qubit, qubit]: - ^^^^^^^^^^^^^^^ -GuppyError: Variable `q` with linear type `qubit` is not used +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/unused.err b/tests/error/linear_errors/unused.err index dc1e47a8..b55f5b54 100644 --- a/tests/error/linear_errors/unused.err +++ b/tests/error/linear_errors/unused.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Linearity violation (at $FILE:14:4) + | +12 | @guppy(module) +13 | def foo(q: qubit @owned) -> int: +14 | x = q + | ^ Variable `x` with linear type `qubit` is leaked -12: @guppy(module) -13: def foo(q: qubit @owned) -> int: -14: x = q - ^ -GuppyError: Variable `x` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `x` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/unused_expr.err b/tests/error/linear_errors/unused_expr.err index c43033b4..8e1f2ac0 100644 --- a/tests/error/linear_errors/unused_expr.err +++ b/tests/error/linear_errors/unused_expr.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Linearity violation (at $FILE:13:4) + | +11 | @guppy(module) +12 | def foo(q: qubit @owned) -> None: +13 | h(q) + | ^^^^ Expression with linear type `qubit` is leaked -11: @guppy(module) -12: def foo(q: qubit @owned) -> None: -13: h(q) - ^^^^ -GuppyTypeError: Value with linear type `qubit` is not used +Help: Consider assigning this value to a local variable + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/unused_field1.err b/tests/error/linear_errors/unused_field1.err index 2993b99b..fddbdc17 100644 --- a/tests/error/linear_errors/unused_field1.err +++ b/tests/error/linear_errors/unused_field1.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:19 +Error: Linearity violation (at $FILE:19:8) + | +17 | +18 | @guppy(module) +19 | def foo(s: MyStruct @owned) -> int: + | ^^^^^^^^^^^^^^^^^^ Field `s.q` with linear type `qubit` is leaked -17: @guppy(module) -18: def foo(s: MyStruct @owned) -> int: - ^^^^^^^^^^^^^^^^^^ -GuppyError: Field `s.q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `s.q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/unused_field2.err b/tests/error/linear_errors/unused_field2.err index 769fc2bb..4e5db4bb 100644 --- a/tests/error/linear_errors/unused_field2.err +++ b/tests/error/linear_errors/unused_field2.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:19 +Error: Linearity violation (at $FILE:19:8) + | +17 | +18 | @guppy(module) +19 | def foo(s: MyStruct @owned) -> qubit: + | ^^^^^^^^^^^^^^^^^^ Field `s.q2` with linear type `qubit` is leaked -17: @guppy(module) -18: def foo(s: MyStruct @owned) -> qubit: - ^^^^^^^^^^^^^^^^^^ -GuppyError: Field `s.q2` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `s.q2` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/unused_field3.err b/tests/error/linear_errors/unused_field3.err index 062a18b9..898ffd35 100644 --- a/tests/error/linear_errors/unused_field3.err +++ b/tests/error/linear_errors/unused_field3.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:25 +Error: Linearity violation (at $FILE:25:8) + | +23 | +24 | @guppy(module) +25 | def foo(s: MyStruct1 @owned) -> int: + | ^^^^^^^^^^^^^^^^^^^ Field `s.x.q2` with linear type `qubit` is leaked -23: @guppy(module) -24: def foo(s: MyStruct1 @owned) -> int: - ^^^^^^^^^^^^^^^^^^^ -GuppyError: Field `s.x.q2` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `s.x.q2` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/unused_same_block.err b/tests/error/linear_errors/unused_same_block.err index f13d4920..b55f5b54 100644 --- a/tests/error/linear_errors/unused_same_block.err +++ b/tests/error/linear_errors/unused_same_block.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Linearity violation (at $FILE:14:4) + | +12 | @guppy(module) +13 | def foo(q: qubit @owned) -> int: +14 | x = q + | ^ Variable `x` with linear type `qubit` is leaked -12: @guppy(module) -13: def foo(q: qubit @owned) -> int: -14: x = q - ^ -GuppyError: Variable `x` with linear type `qubit` is not used +Help: Make sure that `x` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/while_unused.err b/tests/error/linear_errors/while_unused.err index 9f02819b..bd0ac7e8 100644 --- a/tests/error/linear_errors/while_unused.err +++ b/tests/error/linear_errors/while_unused.err @@ -1,7 +1,13 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Linearity violation (at $FILE:12:4) + | +10 | @guppy(module) +11 | def test(n: int) -> None: +12 | q = qubit() + | ^ Variable `q` with linear type `qubit` may be leaked ... + | +14 | while i < n: + | ----- if this expression is `False` -10: @guppy(module) -11: def test(n: int) -> None: -12: q = qubit() - ^ -GuppyError: Variable `q` with linear type `qubit` is not used on all control-flow paths +Help: Make sure that `q` is consumed or returned to avoid the leak + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/nested_errors/linear_capture.err b/tests/error/nested_errors/linear_capture.err index a1055f98..e2509545 100644 --- a/tests/error/nested_errors/linear_capture.err +++ b/tests/error/nested_errors/linear_capture.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:15 +Error: Linearity violation (at $FILE:15:15) + | +13 | def foo(q: qubit @owned) -> qubit: +14 | def bar() -> qubit: +15 | return q + | ^ Variable `q` with linear type qubit cannot be used here + | since `q` is captured from an outer scope + | +13 | def foo(q: qubit @owned) -> qubit: + | --------------- `q` defined here -13: def foo(q: qubit @owned) -> qubit: -14: def bar() -> qubit: -15: return q - ^ -GuppyError: Variable `q` with linear type `qubit` may not be used here because it was defined in an outer scope (at 13:8) +Guppy compilation failed due to 1 previous error From d6ad6b43ed58357d9fa789b60499a15f7588b9e7 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:46:33 +0000 Subject: [PATCH 12/20] refactor: Update expression checker to use new diagnostics (#587) Closes #539 --- examples/demo.ipynb | 13 +- guppylang/checker/expr_checker.py | 438 ++++++++++++------ guppylang/diagnostic.py | 8 +- .../inout_errors/subscript_not_setable.err | 17 +- tests/error/iter_errors/end_missing.err | 15 +- tests/error/iter_errors/end_wrong_type.err | 16 +- tests/error/iter_errors/hasnext_missing.err | 15 +- .../error/iter_errors/hasnext_wrong_type.err | 16 +- tests/error/iter_errors/iter_missing.err | 15 +- tests/error/iter_errors/iter_wrong_type.err | 16 +- tests/error/iter_errors/next_missing.err | 15 +- tests/error/iter_errors/next_wrong_type.err | 16 +- tests/error/linear_errors/for_break.err | 16 +- tests/error/linear_errors/for_return.err | 16 +- tests/error/poly_errors/free_return_var.err | 13 +- tests/error/poly_errors/pass_poly_free.err | 17 +- tests/error/poly_errors/right_to_left.err | 13 +- tests/error/py_errors/guppy_name1.err | 14 +- tests/error/py_errors/guppy_name2.err | 14 +- tests/error/py_errors/list_different_tys.err | 13 +- tests/error/py_errors/list_empty.err | 14 +- tests/error/py_errors/python_err.err | 17 +- tests/error/py_errors/tket2_not_installed.err | 16 +- tests/error/py_errors/unsupported.err | 13 +- .../struct_errors/constructor_missing_arg.err | 15 +- .../constructor_too_many_args.err | 15 +- .../invalid_attribute_access.err | 13 +- tests/error/tensor_errors/poly_tensor.err | 13 +- tests/error/tensor_errors/too_few_args.err | 15 +- tests/error/tensor_errors/too_many_args.err | 15 +- tests/error/type_errors/and_not_bool_left.err | 15 +- .../error/type_errors/and_not_bool_right.err | 15 +- .../type_errors/binary_not_arith_left.err | 13 +- .../type_errors/binary_not_arith_right.err | 13 +- tests/error/type_errors/call_missing_arg.err | 15 +- .../error/type_errors/call_unexpected_arg.err | 15 +- tests/error/type_errors/if_expr_not_bool.err | 15 +- tests/error/type_errors/if_not_bool.err | 15 +- tests/error/type_errors/invert_not_int.err | 13 +- tests/error/type_errors/not_not_bool.err | 15 +- tests/error/type_errors/not_subscriptable.err | 13 +- tests/error/type_errors/or_not_bool_left.err | 15 +- tests/error/type_errors/or_not_bool_right.err | 15 +- tests/error/type_errors/unary_not_arith.err | 13 +- tests/error/type_errors/while_not_bool.err | 15 +- 45 files changed, 683 insertions(+), 394 deletions(-) diff --git a/examples/demo.ipynb b/examples/demo.ipynb index 0f6733fe..a3e64f47 100644 --- a/examples/demo.ipynb +++ b/examples/demo.ipynb @@ -160,13 +160,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "Guppy compilation failed. Error in file :6\n", + "Error: Operator not defined (at :6:11)\n", + " | \n", + "4 | def bad(x: int) -> int:\n", + "5 | # Try to add a tuple to an int\n", + "6 | return x + (x, x)\n", + " | ^^^^^^^^^^ Binary operator `+` not defined for `int` and `(int, int)`\n", "\n", - "4: def bad(x: int) -> int:\n", - "5: # Try to add a tuple to an int\n", - "6: return x + (x, x)\n", - " ^^^^^^^^^^\n", - "GuppyTypeError: Binary operator `+` not defined for arguments of type `int` and `(int, int)`\n" + "Guppy compilation failed due to 1 previous error\n" ] } ], diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index b36ca20a..b87f054c 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -51,7 +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.diagnostic import Error, Help, Note from guppylang.error import ( GuppyError, GuppyTypeError, @@ -77,6 +77,7 @@ TensorCall, TypeApply, ) +from guppylang.span import Span, to_span from guppylang.tys.arg import TypeArg from guppylang.tys.builtin import ( bool_type, @@ -85,6 +86,7 @@ is_list_type, list_type, ) +from guppylang.tys.const import Const from guppylang.tys.param import TypeParam from guppylang.tys.subst import Inst, Subst from guppylang.tys.ty import ( @@ -138,14 +140,219 @@ @dataclass(frozen=True) class TypeMismatchError(Error): - """Error diagnostic for expressions with the wrong type.""" + title: ClassVar[str] = "Type mismatch" + span_label: ClassVar[str] = "Expected {kind} of type `{expected}`, got `{actual}`" expected: Type actual: Type kind: str = "expression" - title: ClassVar[str] = "Type mismatch" - span_label: ClassVar[str] = "Expected {kind} of type `{expected}`, got `{actual}`" + @dataclass(frozen=True) + class CantInferParam(Note): + message: ClassVar[str] = ( + "Couldn't infer an instantiation for type variable `?{type_var}` " + "(higher-rank polymorphic types are not supported)" + ) + type_var: str + + @dataclass(frozen=True) + class CantInstantiateFreeVars(Note): + message: ClassVar[str] = ( + "Can't instantiate parameter `{param}` with type `{illegal_inst}` " + "containing free variables" + ) + param: str + illegal_inst: Type | Const + + +@dataclass(frozen=True) +class TypeInferenceError(Error): + title: ClassVar[str] = "Cannot infer type" + span_label: ClassVar[str] = ( + "Cannot infer type variables in expression of type `{unsolved_ty}`" + ) + unsolved_ty: Type + + +@dataclass(frozen=True) +class UnsupportedError(Error): + title: ClassVar[str] = "Unsupported" + things: str + singular: bool = False + + @property + def rendered_span_label(self) -> str: + is_are = "is" if self.singular else "are" + return f"{self.things} {is_are} not supported" + + +@dataclass(frozen=True) +class IllegalConstant(Error): + title: ClassVar[str] = "Unsupported constant" + span_label: ClassVar[str] = "Type `{ty}` is not supported" + python_ty: type + + +@dataclass(frozen=True) +class IllegalPyExpressionError(Error): + title: ClassVar[str] = "Unsupported Python expression" + span_label: ClassVar[str] = "Expression of type `{python_ty}` is not supported" + python_ty: type + + +@dataclass(frozen=True) +class PyExprNotCPythonError(Error): + title: ClassVar[str] = "Not running CPython" + span_label: ClassVar[str] = ( + "Compile-time `py(...)` expressions are only supported in CPython" + ) + + +@dataclass(frozen=True) +class PyExprNotStaticError(Error): + title: ClassVar[str] = "Not compile-time evaluatable" + span_label: ClassVar[str] = ( + "Guppy variable `{guppy_var}` cannot be accessed in a compile-time `py(...)` " + "expression" + ) + guppy_var: str + + +@dataclass(frozen=True) +class PyExprEvalError(Error): + title: ClassVar[str] = "Python error" + span_label: ClassVar[str] = "Error occurred while evaluating this expression" + message: ClassVar[str] = "Traceback printed below:\n\n{err}" + err: str + + +@dataclass(frozen=True) +class PyExprIncoherentListError(Error): + title: ClassVar[str] = "Unsupported list" + span_label: ClassVar[str] = "List contains elements with different types" + + +@dataclass(frozen=True) +class ModuleMemberNotFoundError(Error): + title: ClassVar[str] = "Not found in module" + span_label: ClassVar[str] = "Module `{module_name}` has no member `{member}`" + module_name: str + member: str + + +@dataclass(frozen=True) +class AttributeNotFoundError(Error): + title: ClassVar[str] = "Attribute not found" + span_label: ClassVar[str] = "Attribute `{attribute}` not found on type `{ty}`" + ty: Type + attribute: str + + +@dataclass(frozen=True) +class UnaryOperatorNotDefinedError(Error): + title: ClassVar[str] = "Operator not defined" + span_label: ClassVar[str] = "Unary operator `{op}` not defined for `{ty}`" + ty: Type + op: str + + +@dataclass(frozen=True) +class BinaryOperatorNotDefinedError(Error): + title: ClassVar[str] = "Operator not defined" + span_label: ClassVar[str] = ( + "Binary operator `{op}` not defined for `{left_ty}` and `{right_ty}`" + ) + left_ty: Type + right_ty: Type + op: str + + +@dataclass(frozen=True) +class BadProtocolError(Error): + title: ClassVar[str] = "Not {is_not}" + span_label: ClassVar[str] = "Expression of type `{ty}` is not {is_not}" + ty: Type + is_not: str + + @dataclass(frozen=True) + class MethodMissing(Help): + message: ClassVar[str] = "Implement missing method: `{method}: {signature}`" + method: str + signature: FunctionType + + @dataclass(frozen=True) + class BadSignature(Help): + message: ClassVar[str] = ( + "Fix signature of method `{ty}.{method}`: Expected `{exp_signature}`, got " + "`{act_signature}`" + ) + ty: Type + method: str + exp_signature: FunctionType + act_signature: FunctionType + + +@dataclass(frozen=True) +class LinearForBreakError(Error): + title: ClassVar[str] = "Break in linear loop" + span_label: ClassVar[str] = "Early exit in linear loops is not allowed" + + @dataclass(frozen=True) + class LinearIteratorType(Note): + span_label: ClassVar[str] = "Iterator has linear type `{ty}`" + ty: Type + + +@dataclass(frozen=True) +class WrongNumberOfArgsError(Error): + title: ClassVar[str] = "" # Custom implementation in `rendered_title` + span_label: ClassVar[str] = "Expected {expected} function arguments, got `{actual}`" + expected: int + actual: int + detailed: bool = True + + @property + def rendered_title(self) -> str: + return ( + "Not enough arguments" + if self.expected > self.actual + else "Too many arguments" + ) + + @property + def rendered_span_label(self) -> str: + if not self.detailed: + return f"Expected {self.expected}, got {self.actual}" + diff = self.expected - self.actual + if diff < 0: + msg = "Unexpected arguments" if diff < -1 else "Unexpected argument" + else: + msg = "Missing arguments" if diff > 1 else "Missing argument" + return f"{msg} (expected {self.expected}, got {self.actual})" + + @dataclass(frozen=True) + class SignatureHint(Note): + message: ClassVar[str] = "Function signature is `{sig}`" + sig: FunctionType + + +@dataclass(frozen=True) +class UnexpectedArgumentError(Error): + title: ClassVar[str] = "Unexpected argument" + span_label: ClassVar[str] = "Expected only {num_args} function arguments" + num_args: int + + +@dataclass(frozen=True) +class Tket2NotInstalled(Error): + title: ClassVar[str] = "Tket2 not installed" + span_label: ClassVar[str] = ( + "Experimental pytket compatibility requires `tket2` to be installed" + ) + + @dataclass(frozen=True) + class InstallInstruction(Help): + message: ClassVar[str] = "Install tket2: `pip install tket2`" class ExprChecker(AstVisitor[tuple[ast.expr, Subst]]): @@ -248,9 +455,7 @@ def visit_DesugaredListComp( def visit_Call(self, node: ast.Call, ty: Type) -> tuple[ast.expr, Subst]: if len(node.keywords) > 0: - raise GuppyError( - "Argument passing by keyword is not supported", node.keywords[0] - ) + raise GuppyError(UnsupportedError(node.keywords[0], "Keyword arguments")) node.func, func_ty = self._synthesize(node.func, allow_free_vars=False) # First handle direct calls of user-defined functions and extension functions @@ -278,8 +483,8 @@ def visit_Call(self, node: ast.Call, ty: Type) -> tuple[ast.expr, Subst]: ): check_function_tensors_enabled(node.func) if any(f.parametrized for f in function_elements): - raise GuppyTypeError( - "Polymorphic functions in tuples are not supported", node.func + raise GuppyError( + UnsupportedError(node.func, "Polymorphic function tensors") ) tensor_ty = function_tensor_signature(function_elements) @@ -300,7 +505,7 @@ def visit_Call(self, node: ast.Call, ty: Type) -> tuple[ast.expr, Subst]: def visit_PyExpr(self, node: PyExpr, ty: Type) -> tuple[ast.expr, Subst]: python_val = eval_py_expr(node, self.ctx) - if act := python_value_to_guppy_type(python_val, node, self.ctx.globals): + if act := python_value_to_guppy_type(python_val, node.value, self.ctx.globals): subst = unify(ty, act, {}) if subst is None: self._fail(ty, act, node) @@ -308,10 +513,7 @@ def visit_PyExpr(self, node: PyExpr, ty: Type) -> tuple[ast.expr, Subst]: subst = {x: s for x, s in subst.items() if x in ty.unsolved_vars} return with_type(act, with_loc(node, ast.Constant(value=python_val))), subst - raise GuppyError( - f"Python expression of type `{type(python_val)}` is not supported by Guppy", - node, - ) + raise GuppyError(IllegalPyExpressionError(node.value, type(python_val))) def generic_visit(self, node: ast.expr, ty: Type) -> tuple[ast.expr, Subst]: # Try to synthesize and then check if we can unify it with the given type @@ -342,9 +544,7 @@ def synthesize( return node, ty node, ty = self.visit(node) if ty.unsolved_vars and not allow_free_vars: - raise GuppyTypeError( - f"Cannot infer type variable in expression of type `{ty}`", node - ) + raise GuppyError(TypeInferenceError(node, ty)) return with_type(ty, node), ty def _check( @@ -356,7 +556,7 @@ def _check( def visit_Constant(self, node: ast.Constant) -> tuple[ast.expr, Type]: ty = python_value_to_guppy_type(node.value, node, self.ctx.globals) if ty is None: - raise GuppyError("Unsupported constant", node) + raise GuppyError(IllegalConstant(node, type(node.value))) return node, ty def visit_Name(self, node: ast.Name) -> tuple[ast.expr, Type]: @@ -390,11 +590,15 @@ def _check_global( ) def visit_Attribute(self, node: ast.Attribute) -> tuple[ast.expr, Type]: - # A `value.attr` attribute access + # A `value.attr` attribute access. 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(node) + attr_span = Span(span.end.shift_left(len(node.attr)), span.end) if module_def := self._is_module_def(node.value): if node.attr not in module_def.globals: raise GuppyError( - f"Module `{module_def.name}` has no member `{node.attr}`", node + ModuleMemberNotFoundError(attr_span, module_def.name, node.attr) ) defn = module_def.globals[node.attr] qual_name = f"{module_def.name}.{defn.name}" @@ -425,12 +629,7 @@ def visit_Attribute(self, node: ast.Attribute) -> tuple[ast.expr, Type]: func.ty.params, ) return with_loc(node, PartialApply(func=name, args=[node.value])), result_ty - raise GuppyTypeError( - f"Expression of type `{ty}` has no attribute `{node.attr}`", - # Unfortunately, `node.attr` doesn't contain source annotations, so we have - # to use `node` as the error location - node, - ) + raise GuppyTypeError(AttributeNotFoundError(attr_span, ty, node.attr)) def _is_module_def(self, node: ast.expr) -> ModuleDef | None: """Checks whether an AST node corresponds to a defined module.""" @@ -449,9 +648,8 @@ def visit_Tuple(self, node: ast.Tuple) -> tuple[ast.expr, Type]: def visit_List(self, node: ast.List) -> tuple[ast.expr, Type]: check_lists_enabled(node) if len(node.elts) == 0: - raise GuppyTypeInferenceError( - "Cannot infer type variable in expression of type `list[?T]`", node - ) + unsolved_ty = list_type(ExistentialTypeVar.fresh("T", False)) + raise GuppyTypeInferenceError(TypeInferenceError(node, unsolved_ty)) node.elts[0], el_ty = self.synthesize(node.elts[0]) node.elts[1:] = [self._check(el, el_ty)[0] for el in node.elts[1:]] return node, list_type(el_ty) @@ -476,9 +674,7 @@ def visit_UnaryOp(self, node: ast.UnaryOp) -> tuple[ast.expr, Type]: func = self.ctx.globals.get_instance_func(op_ty, op) if func is None: raise GuppyTypeError( - f"Unary operator `{display_name}` not defined for argument of type " - f" `{op_ty}`", - node.operand, + UnaryOperatorNotDefinedError(node.operand, op_ty, display_name) ) return func.synthesize_call([node.operand], node, self.ctx) @@ -491,7 +687,7 @@ def _synthesize_binary( `__radd__` on the right operand. """ if op.__class__ not in binary_table: - raise GuppyError("This binary operation is not supported by Guppy.", op) + raise GuppyTypeError(UnsupportedError(node, "Operator", singular=True)) lop, rop, display_name = binary_table[op.__class__] left_expr, left_ty = self.synthesize(left_expr) right_expr, right_ty = self.synthesize(right_expr) @@ -505,9 +701,8 @@ def _synthesize_binary( return func.synthesize_call([right_expr, left_expr], node, self.ctx) raise GuppyTypeError( - f"Binary operator `{display_name}` not defined for arguments of type " - f"`{left_ty}` and `{right_ty}`", - node, + # TODO: Is there a way to get the span of the operator? + BinaryOperatorNotDefinedError(node, left_ty, right_ty, display_name) ) def synthesize_instance_func( @@ -515,7 +710,7 @@ def synthesize_instance_func( node: ast.expr, args: list[ast.expr], func_name: str, - err: str, + description: str, exp_sig: FunctionType | None = None, give_reason: bool = False, ) -> tuple[ast.expr, Type]: @@ -531,17 +726,18 @@ def synthesize_instance_func( node, ty = self.synthesize(node) func = self.ctx.globals.get_instance_func(ty, func_name) if func is None: - reason = f" since it does not implement the `{func_name}` method" - raise GuppyTypeError( - f"Expression of type `{ty}` is {err}{reason if give_reason else ''}", - node, - ) + err = BadProtocolError(node, ty, description) + if give_reason and exp_sig is not None: + err.add_sub_diagnostic( + BadProtocolError.MethodMissing(None, func_name, exp_sig) + ) + raise GuppyTypeError(err) if exp_sig and unify(exp_sig, func.ty.unquantified()[0], {}) is None: - raise GuppyError( - f"Method `{ty}.{func_name}` has signature `{func.ty}`, but " - f"expected `{exp_sig}`", - node, + err = BadProtocolError(node, ty, description) + err.add_sub_diagnostic( + BadProtocolError.BadSignature(None, ty, func_name, exp_sig, func.ty) ) + raise GuppyError(err) return func.synthesize_call([node, *args], node, self.ctx) def visit_BinOp(self, node: ast.BinOp) -> tuple[ast.expr, Type]: @@ -572,7 +768,7 @@ def visit_Subscript(self, node: ast.Subscript) -> tuple[ast.expr, Type]: ExistentialTypeVar.fresh("Val", False), ) getitem_expr, result_ty = self.synthesize_instance_func( - node.value, [item_node], "__getitem__", "not subscriptable", exp_sig + node.value, [item_node], "__getitem__", "subscriptable", exp_sig ) # Subscripting a place is itself a place expr: ast.expr @@ -595,7 +791,7 @@ def visit_Subscript(self, node: ast.Subscript) -> tuple[ast.expr, Type]: def visit_Call(self, node: ast.Call) -> tuple[ast.expr, Type]: if len(node.keywords) > 0: - raise GuppyError("Keyword arguments are not supported", node.keywords[0]) + raise GuppyError(UnsupportedError(node.keywords[0], "Keyword arguments")) node.func, ty = self.synthesize(node.func) # First handle direct calls of user-defined functions and extension functions @@ -620,8 +816,8 @@ def visit_Call(self, node: ast.Call) -> tuple[ast.expr, Type]: ): check_function_tensors_enabled(node.func) if any(f.parametrized for f in function_elems): - raise GuppyTypeError( - "Polymorphic functions in tuples are not supported", node.func + raise GuppyError( + UnsupportedError(node.func, "Polymorphic function tensors") ) tensor_ty = function_tensor_signature(function_elems) @@ -646,7 +842,7 @@ def visit_MakeIter(self, node: MakeIter) -> tuple[ast.expr, Type]: [FuncInput(ty, flags)], ExistentialTypeVar.fresh("Iter", False) ) expr, ty = self.synthesize_instance_func( - node.value, [], "__iter__", "not iterable", exp_sig + node.value, [], "__iter__", "iterable", exp_sig, True ) # If the iterator was created by a `for` loop, we can add some extra checks to @@ -657,11 +853,9 @@ def visit_MakeIter(self, node: MakeIter) -> tuple[ast.expr, Type]: node.origin_node ) if breaks: - raise GuppyTypeError( - f"Loop over iterator with linear type `{ty}` cannot be terminated " - f"prematurely", - breaks[0], - ) + err = LinearForBreakError(breaks[0]) + err.add_sub_diagnostic(LinearForBreakError.LinearIteratorType(node, ty)) + raise GuppyTypeError(err) return expr, ty def visit_IterHasNext(self, node: IterHasNext) -> tuple[ast.expr, Type]: @@ -669,7 +863,7 @@ def visit_IterHasNext(self, node: IterHasNext) -> tuple[ast.expr, Type]: flags = InputFlags.Owned if ty.linear else InputFlags.NoFlags exp_sig = FunctionType([FuncInput(ty, flags)], TupleType([bool_type(), ty])) return self.synthesize_instance_func( - node.value, [], "__hasnext__", "not an iterator", exp_sig, True + node.value, [], "__hasnext__", "an iterator", exp_sig, True ) def visit_IterNext(self, node: IterNext) -> tuple[ast.expr, Type]: @@ -680,7 +874,7 @@ def visit_IterNext(self, node: IterNext) -> tuple[ast.expr, Type]: TupleType([ExistentialTypeVar.fresh("T", False), ty]), ) return self.synthesize_instance_func( - node.value, [], "__next__", "not an iterator", exp_sig, True + node.value, [], "__next__", "an iterator", exp_sig, True ) def visit_IterEnd(self, node: IterEnd) -> tuple[ast.expr, Type]: @@ -688,7 +882,7 @@ def visit_IterEnd(self, node: IterEnd) -> tuple[ast.expr, Type]: flags = InputFlags.Owned if ty.linear else InputFlags.NoFlags exp_sig = FunctionType([FuncInput(ty, flags)], NoneType()) return self.synthesize_instance_func( - node.value, [], "__end__", "not an iterator", exp_sig, True + node.value, [], "__end__", "an iterator", exp_sig, True ) def visit_ListComp(self, node: ast.ListComp) -> tuple[ast.expr, Type]: @@ -702,10 +896,7 @@ def visit_PyExpr(self, node: PyExpr) -> tuple[ast.expr, Type]: if ty := python_value_to_guppy_type(python_val, node, self.ctx.globals): return with_loc(node, ast.Constant(value=python_val)), ty - raise GuppyError( - f"Python expression of type `{type(python_val)}` is not supported by Guppy", - node, - ) + raise GuppyError(IllegalPyExpressionError(node.value, type(python_val))) def visit_NamedExpr(self, node: ast.NamedExpr) -> tuple[ast.expr, Type]: raise InternalGuppyError( @@ -748,20 +939,17 @@ def check_type_against( 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): + param = act.params[i].name if v not in subst: - raise GuppyTypeInferenceError( - f"Expected {kind} of type `{exp}`, got `{act}`. Couldn't infer an " - f"instantiation for parameter `{act.params[i].name}` (higher-rank " - "polymorphic types are not supported)", - node, - ) + err = TypeMismatchError(node, exp, act, kind) + err.add_sub_diagnostic(TypeMismatchError.CantInferParam(None, param)) + raise GuppyTypeInferenceError(err) if subst[v].unsolved_vars: - raise GuppyTypeError( - f"Expected {kind} of type `{exp}`, got `{act}`. Can't instantiate " - f"parameter `{act.params[i]}` with type `{subst[v]}` containing " - "free variables", - node, + err = TypeMismatchError(node, exp, act, kind) + err.add_sub_diagnostic( + TypeMismatchError.CantInstantiateFreeVars(None, param, subst[v]) ) + raise GuppyTypeError(err) inst = [subst[v].to_arg() for v in free_vars] subst = {v: t for v, t in subst.items() if v in exp.unsolved_vars} @@ -778,18 +966,26 @@ def check_type_against( return subst, [] -def check_num_args(exp: int, act: int, node: AstNode) -> None: +def check_num_args( + exp: int, act: int, node: AstNode, sig: FunctionType | None = None +) -> None: """Checks that the correct number of arguments have been passed to a function.""" - if act < exp: - raise GuppyTypeError( - f"Not enough arguments passed (expected {exp}, got {act})", node - ) - if exp < act: - if isinstance(node, ast.Call): - raise GuppyTypeError("Unexpected argument", node.args[exp]) - raise GuppyTypeError( - f"Too many arguments passed (expected {exp}, got {act})", node - ) + if exp == act: + return + span, detailed = to_span(node), False + if isinstance(node, ast.Call): + # We can construct a nicer error span if we know it's a regular call + detailed = True + if exp < act: + span = Span(to_span(node.args[exp]).start, to_span(node.args[-1]).end) + elif act > 0: + span = Span(to_span(node.args[-1]).end, to_span(node).end) + else: + span = Span(to_span(node.func).end, to_span(node).end) + err = WrongNumberOfArgsError(span, exp, act, detailed) + if sig: + err.add_sub_diagnostic(WrongNumberOfArgsError.SignatureHint(None, sig)) + raise GuppyTypeError(err) def type_check_args( @@ -805,7 +1001,7 @@ def type_check_args( Checks that all unification variables can be inferred. """ assert not func_ty.parametrized - check_num_args(len(func_ty.inputs), len(inputs), node) + check_num_args(len(func_ty.inputs), len(inputs), node, func_ty) new_args: list[ast.expr] = [] for inp, func_inp in zip(inputs, func_ty.inputs, strict=True): @@ -824,9 +1020,7 @@ def type_check_args( # We also have to check that we found instantiations for all vars in the return type if not set.issubset(func_ty.output.unsolved_vars, subst.keys()): raise GuppyTypeInferenceError( - f"Cannot infer type variable in expression of type " - f"`{func_ty.output.substitute(subst)}`", - node, + TypeInferenceError(node, func_ty.output.substitute(subst)) ) return new_args, subst @@ -862,7 +1056,7 @@ def check_inout_arg_place(place: Place, ctx: Context, node: PlaceNode) -> Place: setitem_args[0], setitem_args[1:], "__setitem__", - "unable to have subscripted elements borrowed", + "able to borrow subscripted elements", exp_sig, True, ) @@ -878,7 +1072,7 @@ def synthesize_call( instantiation for the quantifiers in the function type. """ assert not func_ty.unsolved_vars - check_num_args(len(func_ty.inputs), len(args), node) + check_num_args(len(func_ty.inputs), len(args), node, func_ty) # Replace quantified variables with free unification variables and try to infer an # instantiation by checking the arguments @@ -909,7 +1103,7 @@ def check_call( expected type, and an instantiation for the quantifiers in the function type. """ assert not func_ty.unsolved_vars - check_num_args(len(func_ty.inputs), len(inputs), node) + check_num_args(len(func_ty.inputs), len(inputs), node, func_ty) # When checking, we can use the information from the expected return type to infer # some type arguments. However, this pushes errors inwards. For example, given a @@ -957,11 +1151,12 @@ def check_call( # Also make sure we found an instantiation for all free vars in the type we're # checking against if not set.issubset(ty.unsolved_vars, subst.keys()): - raise GuppyTypeInferenceError( - f"Expected expression of type `{ty}`, got " - f"`{func_ty.output.substitute(subst)}`. Couldn't infer type variables", - node, + unsolved = (subst.keys() - ty.unsolved_vars).pop() + err = TypeMismatchError(node, ty, func_ty.output.substitute(subst)) + err.add_sub_diagnostic( + TypeMismatchError.CantInferParam(None, unsolved.display_name) ) + raise GuppyTypeInferenceError(err) # Success implies that the substitution is closed assert all(not t.unsolved_vars for t in subst.values()) @@ -1009,23 +1204,11 @@ def to_bool(node: ast.expr, node_ty: Type, ctx: Context) -> tuple[ast.expr, Type """Tries to turn a node into a bool""" if is_bool_type(node_ty): return node, node_ty - - func = ctx.globals.get_instance_func(node_ty, "__bool__") - if func is None: - raise GuppyTypeError( - f"Expression of type `{node_ty}` cannot be interpreted as a `bool`", - node, - ) - - # We could check the return type against bool, but we can give a better error - # message if we synthesise and compare to bool by hand - call, return_ty = func.synthesize_call([node], node, ctx) - if not is_bool_type(return_ty): - raise GuppyTypeError( - f"`__bool__` on type `{node_ty}` returns `{return_ty}` instead of `bool`", - node, - ) - return call, return_ty + synth = ExprSynthesizer(ctx) + exp_sig = FunctionType([FuncInput(node_ty, InputFlags.Inout)], bool_type()) + return synth.synthesize_instance_func( + node, [node], "__bool__", "truthy", exp_sig, True + ) def synthesize_comprehension( @@ -1074,9 +1257,7 @@ def eval_py_expr(node: PyExpr, ctx: Context) -> Any: # The method we used for obtaining the Python variables in scope only works in # CPython (see `get_py_scope()`). if sys.implementation.name != "cpython": - raise GuppyError( - "Compile-time `py(...)` expressions are only supported in CPython", node - ) + raise GuppyError(PyExprNotCPythonError(node)) try: python_val = eval( # noqa: S307 @@ -1085,19 +1266,12 @@ def eval_py_expr(node: PyExpr, ctx: Context) -> Any: DummyEvalDict(ctx, node.value), ) except DummyEvalDict.GuppyVarUsedError as e: - raise GuppyError( - f"Guppy variable `{e.var}` cannot be accessed in a compile-time " - "`py(...)` expression", - e.node or node, - ) from None + raise GuppyError(PyExprNotStaticError(e.node or node, e.var)) from None except Exception as e: # Remove the top frame pointing to the `eval` call from the stack trace tb = e.__traceback__.tb_next if e.__traceback__ else None - raise GuppyError( - "Error occurred while evaluating Python expression:\n\n" - + "".join(traceback.format_exception(type(e), e, tb)), - node, - ) from e + tb_formatted = "".join(traceback.format_exception(type(e), e, tb)) + raise GuppyError(PyExprEvalError(node.value, tb_formatted)) from e return python_val @@ -1139,11 +1313,11 @@ def python_value_to_guppy_type(v: Any, node: ast.expr, globals: Globals) -> Type row_to_type([bool_type()] * v.n_bits), ) except ImportError: - raise GuppyError( - "Experimental pytket compatibility requires `tket2` to be" - " installed. See https://github.com/CQCL/tket2/tree/main/tket2-py", - node, - ) from None + err = Tket2NotInstalled(node) + err.add_sub_diagnostic( + Tket2NotInstalled.InstallInstruction(None) + ) + raise GuppyError(err) from None except ImportError: pass return None @@ -1170,6 +1344,6 @@ def _python_list_to_guppy_type( if ty is None: return None if (subst := unify(ty, el_ty, {})) is None: - raise GuppyError("Python list contains elements with different types", node) + raise GuppyError(PyExprIncoherentListError(node)) el_ty = el_ty.substitute(subst) return list_type(el_ty) diff --git a/guppylang/diagnostic.py b/guppylang/diagnostic.py index 0983a31b..c99f5107 100644 --- a/guppylang/diagnostic.py +++ b/guppylang/diagnostic.py @@ -252,14 +252,16 @@ def render_diagnostic(self, diag: Diagnostic) -> None: max_lineno, is_primary=False, ) - if diag.message: + if diag.rendered_message: self.buffer.append("") self.buffer += textwrap.wrap( - f"{diag.rendered_message}", self.MAX_MESSAGE_LINE_LEN + diag.rendered_message, + self.MAX_MESSAGE_LINE_LEN, + replace_whitespace=False, # Keep \n's in the message ) # Finally, render all sub-diagnostics that have a non-span message for sub_diag in diag.children: - if sub_diag.message: + if sub_diag.rendered_message: self.buffer.append("") self.buffer += textwrap.wrap( f"{self.level_str(sub_diag.level)}: {sub_diag.rendered_message}", diff --git a/tests/error/inout_errors/subscript_not_setable.err b/tests/error/inout_errors/subscript_not_setable.err index d5e29d00..6684c9f7 100644 --- a/tests/error/inout_errors/subscript_not_setable.err +++ b/tests/error/inout_errors/subscript_not_setable.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:24 +Error: Not able to borrow subscripted elements (at $FILE:24:8) + | +22 | @guppy(module) +23 | def test(c: MyImmutableContainer) -> MyImmutableContainer: +24 | foo(c[0]) + | ^^^^ Expression of type `MyImmutableContainer` is not able to + | borrow subscripted elements -22: @guppy(module) -23: def test(c: MyImmutableContainer) -> MyImmutableContainer: -24: foo(c[0]) - ^^^^ -GuppyTypeError: Expression of type `MyImmutableContainer` is unable to have subscripted elements borrowed since it does not implement the `__setitem__` method +Help: Implement missing method: `__setitem__: (MyImmutableContainer, int, qubit +@owned) -> None` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/end_missing.err b/tests/error/iter_errors/end_missing.err index 90ad0692..1b1a62fe 100644 --- a/tests/error/iter_errors/end_missing.err +++ b/tests/error/iter_errors/end_missing.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:33 +Error: Not an iterator (at $FILE:33:13) + | +31 | @guppy(module) +32 | def test(x: MyType) -> None: +33 | for _ in x: + | ^ Expression of type `MyIter` is not an iterator -31: @guppy(module) -32: def test(x: MyType) -> None: -33: for _ in x: - ^ -GuppyTypeError: Expression of type `MyIter` is not an iterator since it does not implement the `__end__` method +Help: Implement missing method: `__end__: MyIter -> None` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/end_wrong_type.err b/tests/error/iter_errors/end_wrong_type.err index 635d6f84..116db466 100644 --- a/tests/error/iter_errors/end_wrong_type.err +++ b/tests/error/iter_errors/end_wrong_type.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:37 +Error: Not an iterator (at $FILE:37:13) + | +35 | @guppy(module) +36 | def test(x: MyType) -> None: +37 | for _ in x: + | ^ Expression of type `MyIter` is not an iterator -35: @guppy(module) -36: def test(x: MyType) -> None: -37: for _ in x: - ^ -GuppyError: Method `MyIter.__end__` has signature `MyIter -> MyIter`, but expected `MyIter -> None` +Help: Fix signature of method `MyIter.__end__`: Expected `MyIter -> None`, got +`MyIter -> MyIter` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/hasnext_missing.err b/tests/error/iter_errors/hasnext_missing.err index 949b776f..cceae81b 100644 --- a/tests/error/iter_errors/hasnext_missing.err +++ b/tests/error/iter_errors/hasnext_missing.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:33 +Error: Not an iterator (at $FILE:33:13) + | +31 | @guppy(module) +32 | def test(x: MyType) -> None: +33 | for _ in x: + | ^ Expression of type `MyIter` is not an iterator -31: @guppy(module) -32: def test(x: MyType) -> None: -33: for _ in x: - ^ -GuppyTypeError: Expression of type `MyIter` is not an iterator since it does not implement the `__hasnext__` method +Help: Implement missing method: `__hasnext__: MyIter -> (bool, MyIter)` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/hasnext_wrong_type.err b/tests/error/iter_errors/hasnext_wrong_type.err index 234e217a..4514710b 100644 --- a/tests/error/iter_errors/hasnext_wrong_type.err +++ b/tests/error/iter_errors/hasnext_wrong_type.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:37 +Error: Not an iterator (at $FILE:37:13) + | +35 | @guppy(module) +36 | def test(x: MyType) -> None: +37 | for _ in x: + | ^ Expression of type `MyIter` is not an iterator -35: @guppy(module) -36: def test(x: MyType) -> None: -37: for _ in x: - ^ -GuppyError: Method `MyIter.__hasnext__` has signature `MyIter -> bool`, but expected `MyIter -> (bool, MyIter)` +Help: Fix signature of method `MyIter.__hasnext__`: Expected `MyIter -> (bool, +MyIter)`, got `MyIter -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/iter_missing.err b/tests/error/iter_errors/iter_missing.err index 3e3c571d..fd8b91f3 100644 --- a/tests/error/iter_errors/iter_missing.err +++ b/tests/error/iter_errors/iter_missing.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:16 +Error: Not iterable (at $FILE:16:13) + | +14 | @guppy(module) +15 | def test(x: MyType) -> None: +16 | for _ in x: + | ^ Expression of type `MyType` is not iterable -14: @guppy(module) -15: def test(x: MyType) -> None: -16: for _ in x: - ^ -GuppyTypeError: Expression of type `MyType` is not iterable +Help: Implement missing method: `__iter__: MyType -> ?Iter` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/iter_wrong_type.err b/tests/error/iter_errors/iter_wrong_type.err index cc10baab..e38a6bdf 100644 --- a/tests/error/iter_errors/iter_wrong_type.err +++ b/tests/error/iter_errors/iter_wrong_type.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:20 +Error: Not iterable (at $FILE:20:13) + | +18 | @guppy(module) +19 | def test(x: MyType) -> None: +20 | for _ in x: + | ^ Expression of type `MyType` is not iterable -18: @guppy(module) -19: def test(x: MyType) -> None: -20: for _ in x: - ^ -GuppyError: Method `MyType.__iter__` has signature `(MyType, int) -> MyType`, but expected `MyType -> ?Iter` +Help: Fix signature of method `MyType.__iter__`: Expected `MyType -> ?Iter`, +got `(MyType, int) -> MyType` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/next_missing.err b/tests/error/iter_errors/next_missing.err index 8ba51c01..8b16fbaa 100644 --- a/tests/error/iter_errors/next_missing.err +++ b/tests/error/iter_errors/next_missing.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:33 +Error: Not an iterator (at $FILE:33:13) + | +31 | @guppy(module) +32 | def test(x: MyType) -> None: +33 | for _ in x: + | ^ Expression of type `MyIter` is not an iterator -31: @guppy(module) -32: def test(x: MyType) -> None: -33: for _ in x: - ^ -GuppyTypeError: Expression of type `MyIter` is not an iterator since it does not implement the `__next__` method +Help: Implement missing method: `__next__: MyIter -> (?T, MyIter)` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/iter_errors/next_wrong_type.err b/tests/error/iter_errors/next_wrong_type.err index b9bac527..61377cd7 100644 --- a/tests/error/iter_errors/next_wrong_type.err +++ b/tests/error/iter_errors/next_wrong_type.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:37 +Error: Not an iterator (at $FILE:37:13) + | +35 | @guppy(module) +36 | def test(x: MyType) -> None: +37 | for _ in x: + | ^ Expression of type `MyIter` is not an iterator -35: @guppy(module) -36: def test(x: MyType) -> None: -37: for _ in x: - ^ -GuppyError: Method `MyIter.__next__` has signature `MyIter -> (MyIter, float)`, but expected `MyIter -> (?T, MyIter)` +Help: Fix signature of method `MyIter.__next__`: Expected `MyIter -> (?T, +MyIter)`, got `MyIter -> (MyIter, float)` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/for_break.err b/tests/error/linear_errors/for_break.err index 889ddaa5..1b2c3074 100644 --- a/tests/error/linear_errors/for_break.err +++ b/tests/error/linear_errors/for_break.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:17 +Error: Break in linear loop (at $FILE:17:12) + | +15 | rs += [q] +16 | if b: +17 | break + | ^^^^^ Early exit in linear loops is not allowed + | +14 | for q, b in qs: + | -- Iterator has linear type `list[(qubit, bool)]` -15: rs += [q] -16: if b: -17: break - ^^^^^ -GuppyTypeError: Loop over iterator with linear type `list[(qubit, bool)]` cannot be terminated prematurely +Guppy compilation failed due to 1 previous error diff --git a/tests/error/linear_errors/for_return.err b/tests/error/linear_errors/for_return.err index 840e4bfe..f0f3d218 100644 --- a/tests/error/linear_errors/for_return.err +++ b/tests/error/linear_errors/for_return.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:17 +Error: Break in linear loop (at $FILE:17:12) + | +15 | rs += [q] +16 | if b: +17 | return [] + | ^^^^^^^^^ Early exit in linear loops is not allowed + | +14 | for q, b in qs: + | -- Iterator has linear type `list[(qubit, bool)]` -15: rs += [q] -16: if b: -17: return [] - ^^^^^^^^^ -GuppyTypeError: Loop over iterator with linear type `list[(qubit, bool)]` cannot be terminated prematurely +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/free_return_var.err b/tests/error/poly_errors/free_return_var.err index bd5e522c..91525870 100644 --- a/tests/error/poly_errors/free_return_var.err +++ b/tests/error/poly_errors/free_return_var.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:17 +Error: Cannot infer type (at $FILE:17:8) + | +15 | @guppy(module) +16 | def main() -> None: +17 | x = foo() + | ^^^^^ Cannot infer type variables in expression of type `?T` -15: @guppy(module) -16: def main() -> None: -17: x = foo() - ^^^^^ -GuppyTypeInferenceError: Cannot infer type variable in expression of type `?T` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/pass_poly_free.err b/tests/error/poly_errors/pass_poly_free.err index 945cafb0..3520a444 100644 --- a/tests/error/poly_errors/pass_poly_free.err +++ b/tests/error/poly_errors/pass_poly_free.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:24 +Error: Type mismatch (at $FILE:24:8) + | +22 | @guppy(module) +23 | def main() -> None: +24 | foo(bar) + | ^^^ Expected argument of type `?T -> ?T`, got `forall T. T -> + | T` -22: @guppy(module) -23: def main() -> None: -24: foo(bar) - ^^^ -GuppyTypeInferenceError: Expected argument of type `?T -> ?T`, got `forall T. T -> T`. Couldn't infer an instantiation for parameter `T` (higher-rank polymorphic types are not supported) +Note: Couldn't infer an instantiation for type variable `?T` (higher-rank +polymorphic types are not supported) + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/right_to_left.err b/tests/error/poly_errors/right_to_left.err index b488be7d..aabeec9c 100644 --- a/tests/error/poly_errors/right_to_left.err +++ b/tests/error/poly_errors/right_to_left.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:22 +Error: Cannot infer type (at $FILE:22:8) + | +20 | @guppy(module) +21 | def main() -> None: +22 | bar(foo(), 42) + | ^^^^^ Cannot infer type variables in expression of type `?T` -20: @guppy(module) -21: def main() -> None: -22: bar(foo(), 42) - ^^^^^ -GuppyTypeInferenceError: Cannot infer type variable in expression of type `?T` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/guppy_name1.err b/tests/error/py_errors/guppy_name1.err index 1b8d8b62..e2b02304 100644 --- a/tests/error/py_errors/guppy_name1.err +++ b/tests/error/py_errors/guppy_name1.err @@ -1,7 +1,9 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Not compile-time evaluatable (at $FILE:6:14) + | +4 | @compile_guppy +5 | def foo(x: int) -> int: +6 | return py(x + 1) + | ^ Guppy variable `x` cannot be accessed in a compile-time + | `py(...)` expression -4: @compile_guppy -5: def foo(x: int) -> int: -6: return py(x + 1) - ^ -GuppyError: Guppy variable `x` cannot be accessed in a compile-time `py(...)` expression +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/guppy_name2.err b/tests/error/py_errors/guppy_name2.err index 73d2d298..22c88866 100644 --- a/tests/error/py_errors/guppy_name2.err +++ b/tests/error/py_errors/guppy_name2.err @@ -1,7 +1,9 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Not compile-time evaluatable (at $FILE:9:14) + | +7 | @compile_guppy +8 | def foo(x: int) -> int: +9 | return py(x + 1) + | ^ Guppy variable `x` cannot be accessed in a compile-time + | `py(...)` expression -7: @compile_guppy -8: def foo(x: int) -> int: -9: return py(x + 1) - ^ -GuppyError: Guppy variable `x` cannot be accessed in a compile-time `py(...)` expression +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/list_different_tys.err b/tests/error/py_errors/list_different_tys.err index f39f07f0..0848122b 100644 --- a/tests/error/py_errors/list_different_tys.err +++ b/tests/error/py_errors/list_different_tys.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Unsupported list (at $FILE:6:14) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | return py([1, 1.0]) + | ^^^^^^^^ List contains elements with different types -4: @compile_guppy -5: def foo() -> int: -6: return py([1, 1.0]) - ^^^^^^^^^^^^ -GuppyError: Python list contains elements with different types +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/list_empty.err b/tests/error/py_errors/list_empty.err index 1663dcb1..381bd770 100644 --- a/tests/error/py_errors/list_empty.err +++ b/tests/error/py_errors/list_empty.err @@ -1,7 +1,9 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Cannot infer type (at $FILE:6:9) + | +4 | @compile_guppy +5 | def foo() -> None: +6 | xs = py([]) + | ^^^^^^ Cannot infer type variables in expression of type + | `list[?T]` -4: @compile_guppy -5: def foo() -> None: -6: xs = py([]) - ^^^^^^ -GuppyTypeError: Cannot infer type variable in expression of type `list[?T]` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/python_err.err b/tests/error/py_errors/python_err.err index 5d21c3e8..26b12b98 100644 --- a/tests/error/py_errors/python_err.err +++ b/tests/error/py_errors/python_err.err @@ -1,12 +1,15 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Python error (at $FILE:6:14) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | return py(1 / 0) + | ^^^^^ Error occurred while evaluating this expression -4: @compile_guppy -5: def foo() -> int: -6: return py(1 / 0) - ^^^^^^^^^ -GuppyError: Error occurred while evaluating Python expression: +Traceback printed below: Traceback (most recent call last): - File "", line 1, in + File "", +line 1, in ZeroDivisionError: division by zero +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/tket2_not_installed.err b/tests/error/py_errors/tket2_not_installed.err index c6e4eb79..18ebbe9a 100644 --- a/tests/error/py_errors/tket2_not_installed.err +++ b/tests/error/py_errors/tket2_not_installed.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:16 +Error: Tket2 not installed (at $FILE:16:8) + | +14 | @guppy(module) +15 | def foo(q: qubit) -> qubit: +16 | f = py(circ) + | ^^^^^^^^ Experimental pytket compatibility requires `tket2` to be + | installed -14: @guppy(module) -15: def foo(q: qubit) -> qubit: -16: f = py(circ) - ^^^^^^^^ -GuppyError: Experimental pytket compatibility requires `tket2` to be installed. See https://github.com/CQCL/tket2/tree/main/tket2-py +Help: Install tket2: `pip install tket2` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/unsupported.err b/tests/error/py_errors/unsupported.err index 4de7be8c..3ea9296d 100644 --- a/tests/error/py_errors/unsupported.err +++ b/tests/error/py_errors/unsupported.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Unsupported Python expression (at $FILE:6:14) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | return py({1, 2, 3}) + | ^^^^^^^^^ Expression of type `` is not supported -4: @compile_guppy -5: def foo() -> int: -6: return py({1, 2, 3}) - ^^^^^^^^^^^^^ -GuppyError: Python expression of type `` is not supported by Guppy +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/constructor_missing_arg.err b/tests/error/struct_errors/constructor_missing_arg.err index cae3f3b7..5c34b5ab 100644 --- a/tests/error/struct_errors/constructor_missing_arg.err +++ b/tests/error/struct_errors/constructor_missing_arg.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:15 +Error: Not enough arguments (at $FILE:15:12) + | +13 | @guppy(module) +14 | def main() -> None: +15 | MyStruct() + | ^^ Missing argument (expected 1, got 0) -13: @guppy(module) -14: def main() -> None: -15: MyStruct() - ^^^^^^^^^^ -GuppyTypeError: Not enough arguments passed (expected 1, got 0) +Note: Function signature is `int -> MyStruct` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/constructor_too_many_args.err b/tests/error/struct_errors/constructor_too_many_args.err index 4483e46d..64418a88 100644 --- a/tests/error/struct_errors/constructor_too_many_args.err +++ b/tests/error/struct_errors/constructor_too_many_args.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:15 +Error: Too many arguments (at $FILE:15:16) + | +13 | @guppy(module) +14 | def main() -> None: +15 | MyStruct(1, 2, 3) + | ^^^^ Unexpected arguments (expected 1, got 3) -13: @guppy(module) -14: def main() -> None: -15: MyStruct(1, 2, 3) - ^ -GuppyTypeError: Unexpected argument +Note: Function signature is `int -> MyStruct` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/invalid_attribute_access.err b/tests/error/struct_errors/invalid_attribute_access.err index 5b08780c..90aae176 100644 --- a/tests/error/struct_errors/invalid_attribute_access.err +++ b/tests/error/struct_errors/invalid_attribute_access.err @@ -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 + | ^ Attribute `z` not found on type `MyStruct` -13: @guppy(module) -14: def foo(s: MyStruct) -> None: -15: s.z - ^^^ -GuppyTypeError: Expression of type `MyStruct` has no attribute `z` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/tensor_errors/poly_tensor.err b/tests/error/tensor_errors/poly_tensor.err index c6b871e2..3a6c43bc 100644 --- a/tests/error/tensor_errors/poly_tensor.err +++ b/tests/error/tensor_errors/poly_tensor.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Unsupported (at $FILE:14:11) + | +12 | @guppy(module) +13 | def main() -> int: +14 | return (foo, foo)(42, 42) + | ^^^^^^^^^^ Polymorphic function tensors are not supported -12: @guppy(module) -13: def main() -> int: -14: return (foo, foo)(42, 42) - ^^^^^^^^^^ -GuppyTypeError: Polymorphic functions in tuples are not supported +Guppy compilation failed due to 1 previous error diff --git a/tests/error/tensor_errors/too_few_args.err b/tests/error/tensor_errors/too_few_args.err index 3f63bb04..30afbc69 100644 --- a/tests/error/tensor_errors/too_few_args.err +++ b/tests/error/tensor_errors/too_few_args.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not enough arguments (at $FILE:12:24) + | +10 | @guppy(module) +11 | def main() -> int: +12 | return (foo, foo)(42) + | ^ Missing argument (expected 2, got 1) -10: @guppy(module) -11: def main() -> int: -12: return (foo, foo)(42) - ^^^^^^^^^^^^^^ -GuppyTypeError: Not enough arguments passed (expected 2, got 1) +Note: Function signature is `(int, int) -> (int, int)` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/tensor_errors/too_many_args.err b/tests/error/tensor_errors/too_many_args.err index d6e23bfe..b70c5495 100644 --- a/tests/error/tensor_errors/too_many_args.err +++ b/tests/error/tensor_errors/too_many_args.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Too many arguments (at $FILE:12:28) + | +10 | @guppy(module) +11 | def main() -> int: +12 | return (foo, foo)(1, 2, 3) + | ^ Unexpected argument (expected 2, got 3) -10: @guppy(module) -11: def main() -> int: -12: return (foo, foo)(1, 2, 3) - ^ -GuppyTypeError: Unexpected argument +Note: Function signature is `(int, int) -> (int, int)` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_left.err b/tests/error/type_errors/and_not_bool_left.err index e0b73d8f..e8f1154d 100644 --- a/tests/error/type_errors/and_not_bool_left.err +++ b/tests/error/type_errors/and_not_bool_left.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:11) + | +10 | @guppy(module) +11 | def foo(x: NonBool, y: bool) -> bool: +12 | return x and y + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: NonBool, y: bool) -> bool: -12: return x and y - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/and_not_bool_right.err b/tests/error/type_errors/and_not_bool_right.err index 52340434..2cc50224 100644 --- a/tests/error/type_errors/and_not_bool_right.err +++ b/tests/error/type_errors/and_not_bool_right.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:17) + | +10 | @guppy(module) +11 | def foo(x: bool, y: NonBool) -> bool: +12 | return x and y + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: bool, y: NonBool) -> bool: -12: return x and y - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/binary_not_arith_left.err b/tests/error/type_errors/binary_not_arith_left.err index 36bb9a57..85453b71 100644 --- a/tests/error/type_errors/binary_not_arith_left.err +++ b/tests/error/type_errors/binary_not_arith_left.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Operator not defined (at $FILE:6:11) + | +4 | @compile_guppy +5 | def foo(x: int) -> int: +6 | return (1, 1) * 4 + | ^^^^^^^^^^ Binary operator `*` not defined for `(int, int)` and `int` -4: @compile_guppy -5: def foo(x: int) -> int: -6: return (1, 1) * 4 - ^^^^^^^^^^ -GuppyTypeError: Binary operator `*` not defined for arguments of type `(int, int)` and `int` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/binary_not_arith_right.err b/tests/error/type_errors/binary_not_arith_right.err index 7bd8ce2d..b8560ef9 100644 --- a/tests/error/type_errors/binary_not_arith_right.err +++ b/tests/error/type_errors/binary_not_arith_right.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Operator not defined (at $FILE:6:11) + | +4 | @compile_guppy +5 | def foo(x: int) -> int: +6 | return x + True + | ^^^^^^^^ Binary operator `+` not defined for `int` and `bool` -4: @compile_guppy -5: def foo(x: int) -> int: -6: return x + True - ^^^^^^^^ -GuppyTypeError: Binary operator `+` not defined for arguments of type `int` and `bool` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/call_missing_arg.err b/tests/error/type_errors/call_missing_arg.err index 9562fd5a..a1160fd6 100644 --- a/tests/error/type_errors/call_missing_arg.err +++ b/tests/error/type_errors/call_missing_arg.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Not enough arguments (at $FILE:6:14) + | +4 | @compile_guppy +5 | def foo(x: int) -> int: +6 | return foo() + | ^^ Missing argument (expected 1, got 0) -4: @compile_guppy -5: def foo(x: int) -> int: -6: return foo() - ^^^^^ -GuppyTypeError: Not enough arguments passed (expected 1, got 0) +Note: Function signature is `int -> int` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/call_unexpected_arg.err b/tests/error/type_errors/call_unexpected_arg.err index 8fe6436d..5bcf8aad 100644 --- a/tests/error/type_errors/call_unexpected_arg.err +++ b/tests/error/type_errors/call_unexpected_arg.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Too many arguments (at $FILE:6:18) + | +4 | @compile_guppy +5 | def foo(x: int) -> int: +6 | return foo(x, x) + | ^ Unexpected argument (expected 1, got 2) -4: @compile_guppy -5: def foo(x: int) -> int: -6: return foo(x, x) - ^ -GuppyTypeError: Unexpected argument +Note: Function signature is `int -> int` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_expr_not_bool.err b/tests/error/type_errors/if_expr_not_bool.err index 2dfdd0d1..f47719a5 100644 --- a/tests/error/type_errors/if_expr_not_bool.err +++ b/tests/error/type_errors/if_expr_not_bool.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:16) + | +10 | @guppy(module) +11 | def foo(x: NonBool) -> int: +12 | return 1 if x else 0 + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: NonBool) -> int: -12: return 1 if x else 0 - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/if_not_bool.err b/tests/error/type_errors/if_not_bool.err index 91f82b43..31ff6201 100644 --- a/tests/error/type_errors/if_not_bool.err +++ b/tests/error/type_errors/if_not_bool.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:7) + | +10 | @guppy(module) +11 | def foo(x: NonBool) -> int: +12 | if x: + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: NonBool) -> int: -12: if x: - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/invert_not_int.err b/tests/error/type_errors/invert_not_int.err index a26cae58..a3084f7e 100644 --- a/tests/error/type_errors/invert_not_int.err +++ b/tests/error/type_errors/invert_not_int.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Operator not defined (at $FILE:6:12) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | return ~() + | ^^ Unary operator `~` not defined for `()` -4: @compile_guppy -5: def foo() -> int: -6: return ~() - ^^ -GuppyTypeError: Unary operator `~` not defined for argument of type `()` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/not_not_bool.err b/tests/error/type_errors/not_not_bool.err index b29d28d7..3d0214fd 100644 --- a/tests/error/type_errors/not_not_bool.err +++ b/tests/error/type_errors/not_not_bool.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:15) + | +10 | @guppy(module) +11 | def foo(x: NonBool) -> bool: +12 | return not x + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: NonBool) -> bool: -12: return not x - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/not_subscriptable.err b/tests/error/type_errors/not_subscriptable.err index 0611a3bf..ff3aaa0c 100644 --- a/tests/error/type_errors/not_subscriptable.err +++ b/tests/error/type_errors/not_subscriptable.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Not subscriptable (at $FILE:9:4) + | +7 | @guppy(module) +8 | def foo(x: int) -> None: +9 | x[0] + | ^ Expression of type `int` is not subscriptable -7: @guppy(module) -8: def foo(x: int) -> None: -9: x[0] - ^ -GuppyTypeError: Expression of type `int` is not subscriptable +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_left.err b/tests/error/type_errors/or_not_bool_left.err index 76ec236c..0b317734 100644 --- a/tests/error/type_errors/or_not_bool_left.err +++ b/tests/error/type_errors/or_not_bool_left.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:11) + | +10 | @guppy(module) +11 | def foo(x: NonBool, y: bool) -> bool: +12 | return x or y + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: NonBool, y: bool) -> bool: -12: return x or y - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/or_not_bool_right.err b/tests/error/type_errors/or_not_bool_right.err index 768f23a2..570a9e59 100644 --- a/tests/error/type_errors/or_not_bool_right.err +++ b/tests/error/type_errors/or_not_bool_right.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:16) + | +10 | @guppy(module) +11 | def foo(x: bool, y: NonBool) -> bool: +12 | return x or y + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: bool, y: NonBool) -> bool: -12: return x or y - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/unary_not_arith.err b/tests/error/type_errors/unary_not_arith.err index 09ae6847..fa670ccf 100644 --- a/tests/error/type_errors/unary_not_arith.err +++ b/tests/error/type_errors/unary_not_arith.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Operator not defined (at $FILE:6:12) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | return -() + | ^^ Unary operator `-` not defined for `()` -4: @compile_guppy -5: def foo() -> int: -6: return -() - ^^ -GuppyTypeError: Unary operator `-` not defined for argument of type `()` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/while_not_bool.err b/tests/error/type_errors/while_not_bool.err index a855ff9d..00520eb6 100644 --- a/tests/error/type_errors/while_not_bool.err +++ b/tests/error/type_errors/while_not_bool.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not truthy (at $FILE:12:10) + | +10 | @guppy(module) +11 | def foo(x: NonBool) -> int: +12 | while x: + | ^ Expression of type `NonBool` is not truthy -10: @guppy(module) -11: def foo(x: NonBool) -> int: -12: while x: - ^ -GuppyTypeError: Expression of type `NonBool` cannot be interpreted as a `bool` +Help: Implement missing method: `__bool__: NonBool -> bool` + +Guppy compilation failed due to 1 previous error From 08af12ca023e0a4b59bb2dbe0afed15bfd2b28dc Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Tue, 5 Nov 2024 11:05:26 +0000 Subject: [PATCH 13/20] refactor: Move errors into separate module --- guppylang/checker/core.py | 7 +- guppylang/checker/errors/linearity.py | 11 ++ guppylang/checker/errors/py_errors.py | 55 ++++++ guppylang/checker/errors/type_errors.py | 142 ++++++++++++++ guppylang/checker/expr_checker.py | 244 +++--------------------- 5 files changed, 237 insertions(+), 222 deletions(-) create mode 100644 guppylang/checker/errors/py_errors.py create mode 100644 guppylang/checker/errors/type_errors.py diff --git a/guppylang/checker/core.py b/guppylang/checker/core.py index 791a5ab1..c368cedf 100644 --- a/guppylang/checker/core.py +++ b/guppylang/checker/core.py @@ -54,8 +54,13 @@ @dataclass(frozen=True) class UnsupportedError(Error): title: ClassVar[str] = "Unsupported" - span_label: ClassVar[str] = "{things} are not supported" things: str + singular: bool = False + + @property + def rendered_span_label(self) -> str: + is_are = "is" if self.singular else "are" + return f"{self.things} {is_are} not supported" #: A "place" is a description for a storage location of a local value that users diff --git a/guppylang/checker/errors/linearity.py b/guppylang/checker/errors/linearity.py index 68638ac9..cc9075f2 100644 --- a/guppylang/checker/errors/linearity.py +++ b/guppylang/checker/errors/linearity.py @@ -257,3 +257,14 @@ class Captured(Note): "This expression with linear type `{ty}` is implicitly captured" ) ty: Type + + +@dataclass(frozen=True) +class LinearForBreakError(Error): + title: ClassVar[str] = "Break in linear loop" + span_label: ClassVar[str] = "Early exit in linear loops is not allowed" + + @dataclass(frozen=True) + class LinearIteratorType(Note): + span_label: ClassVar[str] = "Iterator has linear type `{ty}`" + ty: Type diff --git a/guppylang/checker/errors/py_errors.py b/guppylang/checker/errors/py_errors.py new file mode 100644 index 00000000..e3377888 --- /dev/null +++ b/guppylang/checker/errors/py_errors.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +from typing import ClassVar + +from guppylang.diagnostic import Error, Help + + +@dataclass(frozen=True) +class IllegalPyExpressionError(Error): + title: ClassVar[str] = "Unsupported Python expression" + span_label: ClassVar[str] = "Expression of type `{python_ty}` is not supported" + python_ty: type + + +@dataclass(frozen=True) +class PyExprNotCPythonError(Error): + title: ClassVar[str] = "Not running CPython" + span_label: ClassVar[str] = ( + "Compile-time `py(...)` expressions are only supported in CPython" + ) + + +@dataclass(frozen=True) +class PyExprNotStaticError(Error): + title: ClassVar[str] = "Not compile-time evaluatable" + span_label: ClassVar[str] = ( + "Guppy variable `{guppy_var}` cannot be accessed in a compile-time `py(...)` " + "expression" + ) + guppy_var: str + + +@dataclass(frozen=True) +class PyExprEvalError(Error): + title: ClassVar[str] = "Python error" + span_label: ClassVar[str] = "Error occurred while evaluating this expression" + message: ClassVar[str] = "Traceback printed below:\n\n{err}" + err: str + + +@dataclass(frozen=True) +class PyExprIncoherentListError(Error): + title: ClassVar[str] = "Unsupported list" + span_label: ClassVar[str] = "List contains elements with different types" + + +@dataclass(frozen=True) +class Tket2NotInstalled(Error): + title: ClassVar[str] = "Tket2 not installed" + span_label: ClassVar[str] = ( + "Experimental pytket compatibility requires `tket2` to be installed" + ) + + @dataclass(frozen=True) + class InstallInstruction(Help): + message: ClassVar[str] = "Install tket2: `pip install tket2`" diff --git a/guppylang/checker/errors/type_errors.py b/guppylang/checker/errors/type_errors.py new file mode 100644 index 00000000..eed98fd6 --- /dev/null +++ b/guppylang/checker/errors/type_errors.py @@ -0,0 +1,142 @@ +from dataclasses import dataclass +from typing import ClassVar + +from guppylang.diagnostic import Error, Help, Note +from guppylang.tys.const import Const +from guppylang.tys.ty import FunctionType, Type + + +@dataclass(frozen=True) +class TypeMismatchError(Error): + title: ClassVar[str] = "Type mismatch" + span_label: ClassVar[str] = "Expected {kind} of type `{expected}`, got `{actual}`" + + expected: Type + actual: Type + kind: str = "expression" + + @dataclass(frozen=True) + class CantInferParam(Note): + message: ClassVar[str] = ( + "Couldn't infer an instantiation for type variable `?{type_var}` " + "(higher-rank polymorphic types are not supported)" + ) + type_var: str + + @dataclass(frozen=True) + class CantInstantiateFreeVars(Note): + message: ClassVar[str] = ( + "Can't instantiate parameter `{param}` with type `{illegal_inst}` " + "containing free variables" + ) + param: str + illegal_inst: Type | Const + + +@dataclass(frozen=True) +class TypeInferenceError(Error): + title: ClassVar[str] = "Cannot infer type" + span_label: ClassVar[str] = ( + "Cannot infer type variables in expression of type `{unsolved_ty}`" + ) + unsolved_ty: Type + + +@dataclass(frozen=True) +class IllegalConstant(Error): + title: ClassVar[str] = "Unsupported constant" + span_label: ClassVar[str] = "Type `{ty}` is not supported" + python_ty: type + + +@dataclass(frozen=True) +class ModuleMemberNotFoundError(Error): + title: ClassVar[str] = "Not found in module" + span_label: ClassVar[str] = "Module `{module_name}` has no member `{member}`" + module_name: str + member: str + + +@dataclass(frozen=True) +class AttributeNotFoundError(Error): + title: ClassVar[str] = "Attribute not found" + span_label: ClassVar[str] = "Attribute `{attribute}` not found on type `{ty}`" + ty: Type + attribute: str + + +@dataclass(frozen=True) +class UnaryOperatorNotDefinedError(Error): + title: ClassVar[str] = "Operator not defined" + span_label: ClassVar[str] = "Unary operator `{op}` not defined for `{ty}`" + ty: Type + op: str + + +@dataclass(frozen=True) +class BinaryOperatorNotDefinedError(Error): + title: ClassVar[str] = "Operator not defined" + span_label: ClassVar[str] = ( + "Binary operator `{op}` not defined for `{left_ty}` and `{right_ty}`" + ) + left_ty: Type + right_ty: Type + op: str + + +@dataclass(frozen=True) +class BadProtocolError(Error): + title: ClassVar[str] = "Not {is_not}" + span_label: ClassVar[str] = "Expression of type `{ty}` is not {is_not}" + ty: Type + is_not: str + + @dataclass(frozen=True) + class MethodMissing(Help): + message: ClassVar[str] = "Implement missing method: `{method}: {signature}`" + method: str + signature: FunctionType + + @dataclass(frozen=True) + class BadSignature(Help): + message: ClassVar[str] = ( + "Fix signature of method `{ty}.{method}`: Expected `{exp_signature}`, got " + "`{act_signature}`" + ) + ty: Type + method: str + exp_signature: FunctionType + act_signature: FunctionType + + +@dataclass(frozen=True) +class WrongNumberOfArgsError(Error): + title: ClassVar[str] = "" # Custom implementation in `rendered_title` + span_label: ClassVar[str] = "Expected {expected} function arguments, got `{actual}`" + expected: int + actual: int + detailed: bool = True + + @property + def rendered_title(self) -> str: + return ( + "Not enough arguments" + if self.expected > self.actual + else "Too many arguments" + ) + + @property + def rendered_span_label(self) -> str: + if not self.detailed: + return f"Expected {self.expected}, got {self.actual}" + diff = self.expected - self.actual + if diff < 0: + msg = "Unexpected arguments" if diff < -1 else "Unexpected argument" + else: + msg = "Missing arguments" if diff > 1 else "Missing argument" + return f"{msg} (expected {self.expected}, got {self.actual})" + + @dataclass(frozen=True) + class SignatureHint(Note): + message: ClassVar[str] = "Function signature is `{sig}`" + sig: FunctionType diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index b87f054c..c4243548 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -24,8 +24,8 @@ import sys import traceback from contextlib import suppress -from dataclasses import dataclass, replace -from typing import Any, ClassVar, NoReturn, cast +from dataclasses import replace +from typing import Any, NoReturn, cast from guppylang.ast_util import ( AstNode, @@ -45,13 +45,33 @@ Locals, Place, SubscriptAccess, + UnsupportedError, Variable, ) +from guppylang.checker.errors.linearity import LinearForBreakError +from guppylang.checker.errors.py_errors import ( + IllegalPyExpressionError, + PyExprEvalError, + PyExprIncoherentListError, + PyExprNotCPythonError, + PyExprNotStaticError, + Tket2NotInstalled, +) +from guppylang.checker.errors.type_errors import ( + AttributeNotFoundError, + BadProtocolError, + BinaryOperatorNotDefinedError, + IllegalConstant, + ModuleMemberNotFoundError, + TypeInferenceError, + TypeMismatchError, + UnaryOperatorNotDefinedError, + WrongNumberOfArgsError, +) from guppylang.definition.common import Definition from guppylang.definition.module import ModuleDef from guppylang.definition.ty import TypeDef from guppylang.definition.value import CallableDef, ValueDef -from guppylang.diagnostic import Error, Help, Note from guppylang.error import ( GuppyError, GuppyTypeError, @@ -86,7 +106,6 @@ is_list_type, list_type, ) -from guppylang.tys.const import Const from guppylang.tys.param import TypeParam from guppylang.tys.subst import Inst, Subst from guppylang.tys.ty import ( @@ -138,223 +157,6 @@ } # fmt: skip -@dataclass(frozen=True) -class TypeMismatchError(Error): - title: ClassVar[str] = "Type mismatch" - span_label: ClassVar[str] = "Expected {kind} of type `{expected}`, got `{actual}`" - - expected: Type - actual: Type - kind: str = "expression" - - @dataclass(frozen=True) - class CantInferParam(Note): - message: ClassVar[str] = ( - "Couldn't infer an instantiation for type variable `?{type_var}` " - "(higher-rank polymorphic types are not supported)" - ) - type_var: str - - @dataclass(frozen=True) - class CantInstantiateFreeVars(Note): - message: ClassVar[str] = ( - "Can't instantiate parameter `{param}` with type `{illegal_inst}` " - "containing free variables" - ) - param: str - illegal_inst: Type | Const - - -@dataclass(frozen=True) -class TypeInferenceError(Error): - title: ClassVar[str] = "Cannot infer type" - span_label: ClassVar[str] = ( - "Cannot infer type variables in expression of type `{unsolved_ty}`" - ) - unsolved_ty: Type - - -@dataclass(frozen=True) -class UnsupportedError(Error): - title: ClassVar[str] = "Unsupported" - things: str - singular: bool = False - - @property - def rendered_span_label(self) -> str: - is_are = "is" if self.singular else "are" - return f"{self.things} {is_are} not supported" - - -@dataclass(frozen=True) -class IllegalConstant(Error): - title: ClassVar[str] = "Unsupported constant" - span_label: ClassVar[str] = "Type `{ty}` is not supported" - python_ty: type - - -@dataclass(frozen=True) -class IllegalPyExpressionError(Error): - title: ClassVar[str] = "Unsupported Python expression" - span_label: ClassVar[str] = "Expression of type `{python_ty}` is not supported" - python_ty: type - - -@dataclass(frozen=True) -class PyExprNotCPythonError(Error): - title: ClassVar[str] = "Not running CPython" - span_label: ClassVar[str] = ( - "Compile-time `py(...)` expressions are only supported in CPython" - ) - - -@dataclass(frozen=True) -class PyExprNotStaticError(Error): - title: ClassVar[str] = "Not compile-time evaluatable" - span_label: ClassVar[str] = ( - "Guppy variable `{guppy_var}` cannot be accessed in a compile-time `py(...)` " - "expression" - ) - guppy_var: str - - -@dataclass(frozen=True) -class PyExprEvalError(Error): - title: ClassVar[str] = "Python error" - span_label: ClassVar[str] = "Error occurred while evaluating this expression" - message: ClassVar[str] = "Traceback printed below:\n\n{err}" - err: str - - -@dataclass(frozen=True) -class PyExprIncoherentListError(Error): - title: ClassVar[str] = "Unsupported list" - span_label: ClassVar[str] = "List contains elements with different types" - - -@dataclass(frozen=True) -class ModuleMemberNotFoundError(Error): - title: ClassVar[str] = "Not found in module" - span_label: ClassVar[str] = "Module `{module_name}` has no member `{member}`" - module_name: str - member: str - - -@dataclass(frozen=True) -class AttributeNotFoundError(Error): - title: ClassVar[str] = "Attribute not found" - span_label: ClassVar[str] = "Attribute `{attribute}` not found on type `{ty}`" - ty: Type - attribute: str - - -@dataclass(frozen=True) -class UnaryOperatorNotDefinedError(Error): - title: ClassVar[str] = "Operator not defined" - span_label: ClassVar[str] = "Unary operator `{op}` not defined for `{ty}`" - ty: Type - op: str - - -@dataclass(frozen=True) -class BinaryOperatorNotDefinedError(Error): - title: ClassVar[str] = "Operator not defined" - span_label: ClassVar[str] = ( - "Binary operator `{op}` not defined for `{left_ty}` and `{right_ty}`" - ) - left_ty: Type - right_ty: Type - op: str - - -@dataclass(frozen=True) -class BadProtocolError(Error): - title: ClassVar[str] = "Not {is_not}" - span_label: ClassVar[str] = "Expression of type `{ty}` is not {is_not}" - ty: Type - is_not: str - - @dataclass(frozen=True) - class MethodMissing(Help): - message: ClassVar[str] = "Implement missing method: `{method}: {signature}`" - method: str - signature: FunctionType - - @dataclass(frozen=True) - class BadSignature(Help): - message: ClassVar[str] = ( - "Fix signature of method `{ty}.{method}`: Expected `{exp_signature}`, got " - "`{act_signature}`" - ) - ty: Type - method: str - exp_signature: FunctionType - act_signature: FunctionType - - -@dataclass(frozen=True) -class LinearForBreakError(Error): - title: ClassVar[str] = "Break in linear loop" - span_label: ClassVar[str] = "Early exit in linear loops is not allowed" - - @dataclass(frozen=True) - class LinearIteratorType(Note): - span_label: ClassVar[str] = "Iterator has linear type `{ty}`" - ty: Type - - -@dataclass(frozen=True) -class WrongNumberOfArgsError(Error): - title: ClassVar[str] = "" # Custom implementation in `rendered_title` - span_label: ClassVar[str] = "Expected {expected} function arguments, got `{actual}`" - expected: int - actual: int - detailed: bool = True - - @property - def rendered_title(self) -> str: - return ( - "Not enough arguments" - if self.expected > self.actual - else "Too many arguments" - ) - - @property - def rendered_span_label(self) -> str: - if not self.detailed: - return f"Expected {self.expected}, got {self.actual}" - diff = self.expected - self.actual - if diff < 0: - msg = "Unexpected arguments" if diff < -1 else "Unexpected argument" - else: - msg = "Missing arguments" if diff > 1 else "Missing argument" - return f"{msg} (expected {self.expected}, got {self.actual})" - - @dataclass(frozen=True) - class SignatureHint(Note): - message: ClassVar[str] = "Function signature is `{sig}`" - sig: FunctionType - - -@dataclass(frozen=True) -class UnexpectedArgumentError(Error): - title: ClassVar[str] = "Unexpected argument" - span_label: ClassVar[str] = "Expected only {num_args} function arguments" - num_args: int - - -@dataclass(frozen=True) -class Tket2NotInstalled(Error): - title: ClassVar[str] = "Tket2 not installed" - span_label: ClassVar[str] = ( - "Experimental pytket compatibility requires `tket2` to be installed" - ) - - @dataclass(frozen=True) - class InstallInstruction(Help): - message: ClassVar[str] = "Install tket2: `pip install tket2`" - - class ExprChecker(AstVisitor[tuple[ast.expr, Subst]]): """Checks an expression against a type and produces a new type-annotated AST. From cbe08300693150a71ec8cf304a758e3d610eb84e Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:38:52 +0000 Subject: [PATCH 14/20] feat: Update statement checker to use new diagnostics (#621) Closes #540 --- guppylang/checker/errors/type_errors.py | 53 ++++++++++++++++-- guppylang/checker/stmt_checker.py | 56 +++++++++++-------- tests/error/struct_errors/assign_call.err | 16 ++++-- .../invalid_attribute_access.err | 2 +- .../invalid_attribute_assign1.err | 13 +++-- .../invalid_attribute_assign2.err | 14 +++-- .../invalid_attribute_assign3.err | 14 +++-- .../error/struct_errors/mutate_classical.err | 13 +++-- tests/error/type_errors/unpack_not_enough.err | 13 +++-- tests/error/type_errors/unpack_too_many.err | 13 +++-- 10 files changed, 137 insertions(+), 70 deletions(-) diff --git a/guppylang/checker/errors/type_errors.py b/guppylang/checker/errors/type_errors.py index eed98fd6..1b069ed6 100644 --- a/guppylang/checker/errors/type_errors.py +++ b/guppylang/checker/errors/type_errors.py @@ -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) @@ -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" @@ -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 @@ -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 diff --git a/guppylang/checker/stmt_checker.py b/guppylang/checker/stmt_checker.py index 321ec3cf..ba795fff 100644 --- a/guppylang/checker/stmt_checker.py +++ b/guppylang/checker/stmt_checker.py @@ -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 @@ -57,24 +64,26 @@ 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 @@ -82,15 +91,16 @@ def _check_assign(self, lhs: ast.expr, ty: Type, node: ast.stmt) -> ast.expr: # 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))) @@ -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) @@ -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: @@ -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! diff --git a/tests/error/struct_errors/assign_call.err b/tests/error/struct_errors/assign_call.err index 801c27ba..629ac3c4 100644 --- a/tests/error/struct_errors/assign_call.err +++ b/tests/error/struct_errors/assign_call.err @@ -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 diff --git a/tests/error/struct_errors/invalid_attribute_access.err b/tests/error/struct_errors/invalid_attribute_access.err index 90aae176..b4456dac 100644 --- a/tests/error/struct_errors/invalid_attribute_access.err +++ b/tests/error/struct_errors/invalid_attribute_access.err @@ -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 diff --git a/tests/error/struct_errors/invalid_attribute_assign1.err b/tests/error/struct_errors/invalid_attribute_assign1.err index ca4683cc..d1cb6a78 100644 --- a/tests/error/struct_errors/invalid_attribute_assign1.err +++ b/tests/error/struct_errors/invalid_attribute_assign1.err @@ -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 diff --git a/tests/error/struct_errors/invalid_attribute_assign2.err b/tests/error/struct_errors/invalid_attribute_assign2.err index bfbf4825..104bd821 100644 --- a/tests/error/struct_errors/invalid_attribute_assign2.err +++ b/tests/error/struct_errors/invalid_attribute_assign2.err @@ -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 diff --git a/tests/error/struct_errors/invalid_attribute_assign3.err b/tests/error/struct_errors/invalid_attribute_assign3.err index cc3759ef..d71ee4a4 100644 --- a/tests/error/struct_errors/invalid_attribute_assign3.err +++ b/tests/error/struct_errors/invalid_attribute_assign3.err @@ -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 diff --git a/tests/error/struct_errors/mutate_classical.err b/tests/error/struct_errors/mutate_classical.err index e6e30784..39fdcf33 100644 --- a/tests/error/struct_errors/mutate_classical.err +++ b/tests/error/struct_errors/mutate_classical.err @@ -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 diff --git a/tests/error/type_errors/unpack_not_enough.err b/tests/error/type_errors/unpack_not_enough.err index d9523c30..7fa44ef1 100644 --- a/tests/error/type_errors/unpack_not_enough.err +++ b/tests/error/type_errors/unpack_not_enough.err @@ -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 diff --git a/tests/error/type_errors/unpack_too_many.err b/tests/error/type_errors/unpack_too_many.err index 44ffb60f..8a74230c 100644 --- a/tests/error/type_errors/unpack_too_many.err +++ b/tests/error/type_errors/unpack_too_many.err @@ -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 From f7a0cdc668c54fda2f8c74f292eb758bb21f4ad6 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:48:27 +0000 Subject: [PATCH 15/20] feat: Update top-level definitions to use new diagnostics (#604) --- guppylang/checker/errors/generic.py | 45 +++++++++ guppylang/decorator.py | 14 +-- guppylang/definition/common.py | 10 ++ guppylang/definition/custom.py | 72 ++++++++++---- guppylang/definition/declaration.py | 13 ++- guppylang/definition/function.py | 16 +-- guppylang/definition/struct.py | 99 ++++++++++++------- guppylang/definition/value.py | 3 +- guppylang/diagnostic.py | 9 ++ .../error/misc_errors/custom_no_signature.err | 11 +++ .../error/misc_errors/custom_no_signature.py | 11 +++ tests/error/misc_errors/custom_not_empty.err | 8 ++ tests/error/misc_errors/custom_not_empty.py | 12 +++ .../error/misc_errors/not_a_function_def.err | 8 ++ tests/error/misc_errors/not_a_function_def.py | 9 ++ tests/error/misc_errors/not_higher_order.err | 8 ++ tests/error/misc_errors/not_higher_order.py | 6 ++ tests/error/misc_errors/unexpected_body.err | 8 ++ tests/error/misc_errors/unexpected_body.py | 12 +++ tests/error/poly_errors/define.err | 14 ++- tests/error/struct_errors/default.err | 13 +-- tests/error/struct_errors/duplicate_field.err | 13 +-- .../struct_errors/func_overrides_field1.err | 15 +-- .../struct_errors/func_overrides_field2.err | 15 +-- tests/error/struct_errors/inheritance.err | 12 ++- tests/error/struct_errors/invalid_generic.err | 12 ++- tests/error/struct_errors/keywords.err | 12 ++- .../error/struct_errors/mutual_recursive.err | 13 +-- tests/error/struct_errors/non_guppy_func.err | 17 ++-- tests/error/struct_errors/not_a_class_def.err | 10 ++ tests/error/struct_errors/not_a_class_def.py | 12 +++ tests/error/struct_errors/recursive.err | 13 +-- .../error/struct_errors/repeated_generic.err | 8 ++ tests/error/struct_errors/repeated_generic.py | 21 ++++ tests/error/struct_errors/stray_docstring.err | 13 +-- tests/error/struct_errors/type_missing1.err | 13 +-- tests/error/struct_errors/type_missing2.err | 13 +-- 37 files changed, 461 insertions(+), 152 deletions(-) create mode 100644 guppylang/checker/errors/generic.py create mode 100644 tests/error/misc_errors/custom_no_signature.err create mode 100644 tests/error/misc_errors/custom_no_signature.py create mode 100644 tests/error/misc_errors/custom_not_empty.err create mode 100644 tests/error/misc_errors/custom_not_empty.py create mode 100644 tests/error/misc_errors/not_a_function_def.err create mode 100644 tests/error/misc_errors/not_a_function_def.py create mode 100644 tests/error/misc_errors/not_higher_order.err create mode 100644 tests/error/misc_errors/not_higher_order.py create mode 100644 tests/error/misc_errors/unexpected_body.err create mode 100644 tests/error/misc_errors/unexpected_body.py create mode 100644 tests/error/struct_errors/not_a_class_def.err create mode 100644 tests/error/struct_errors/not_a_class_def.py create mode 100644 tests/error/struct_errors/repeated_generic.err create mode 100644 tests/error/struct_errors/repeated_generic.py diff --git a/guppylang/checker/errors/generic.py b/guppylang/checker/errors/generic.py new file mode 100644 index 00000000..f331f8d5 --- /dev/null +++ b/guppylang/checker/errors/generic.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import ClassVar + +from guppylang.diagnostic import Error + + +@dataclass(frozen=True) +class UnsupportedError(Error): + title: ClassVar[str] = "Unsupported" + span_label: ClassVar[str] = "{things} {is_are} not supported{extra}" + things: str + singular: bool = False + unsupported_in: str = "" + + @property + def is_are(self) -> str: + return "is" if self.singular else "are" + + @property + def extra(self) -> str: + return f" in {self.unsupported_in}" if self.unsupported_in else "" + + +@dataclass(frozen=True) +class UnexpectedError(Error): + title: ClassVar[str] = "Unexpected {things}" + span_label: ClassVar[str] = "Unexpected {things}{extra}" + things: str + unexpected_in: str = "" + + @property + def extra(self) -> str: + return f" in {self.unexpected_in}" if self.unexpected_in else "" + + +@dataclass(frozen=True) +class ExpectedError(Error): + title: ClassVar[str] = "Expected {things}" + span_label: ClassVar[str] = "Expected {things}{extra}" + things: str + got: str = "" + + @property + def extra(self) -> str: + return f", got {self.got}" if self.got else "" diff --git a/guppylang/decorator.py b/guppylang/decorator.py index a19f4328..857cb443 100644 --- a/guppylang/decorator.py +++ b/guppylang/decorator.py @@ -12,7 +12,7 @@ from hugr.package import FuncDefnPointer, ModulePointer import guppylang -from guppylang.ast_util import annotate_location, has_empty_body +from guppylang.ast_util import annotate_location from guppylang.definition.common import DefId, Definition from guppylang.definition.const import RawConstDef from guppylang.definition.custom import ( @@ -28,7 +28,6 @@ from guppylang.definition.function import ( CompiledFunctionDef, RawFunctionDef, - parse_py_func, ) from guppylang.definition.parameter import ConstVarDef, TypeVarDef from guppylang.definition.struct import RawStructDef @@ -309,17 +308,12 @@ def custom( mod = module or self.get_module() def dec(f: PyFunc) -> RawCustomFunctionDef: - func_ast, docstring = parse_py_func(f, self._sources) - if not has_empty_body(func_ast): - raise GuppyError( - "Body of custom function declaration must be empty", - func_ast.body[0], - ) call_checker = checker or DefaultCallChecker() func = RawCustomFunctionDef( DefId.fresh(mod), - name or func_ast.name, - func_ast, + name or f.__name__, + None, + f, call_checker, compiler or NotImplementedCallCompiler(), higher_order_value, diff --git a/guppylang/definition/common.py b/guppylang/definition/common.py index da70406d..68085a60 100644 --- a/guppylang/definition/common.py +++ b/guppylang/definition/common.py @@ -7,6 +7,7 @@ from hugr.build.dfg import DefinitionBuilder, OpVar +from guppylang.diagnostic import Fatal from guppylang.span import SourceMap if TYPE_CHECKING: @@ -157,3 +158,12 @@ def compile_inner(self, globals: "CompiledGlobals") -> None: Opposed to `CompilableDef.compile()`, we have access to all other compiled definitions here, which allows things like mutual recursion. """ + + +@dataclass(frozen=True) +class UnknownSourceError(Fatal): + title: ClassVar[str] = "Cannot find source" + message: ClassVar[str] = ( + "Unable to look up the source code for Python object `{obj}`" + ) + obj: object diff --git a/guppylang/definition/custom.py b/guppylang/definition/custom.py index ee250d7b..5f5ac70d 100644 --- a/guppylang/definition/custom.py +++ b/guppylang/definition/custom.py @@ -2,18 +2,20 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Sequence from dataclasses import dataclass, field +from typing import TYPE_CHECKING, ClassVar from hugr import Wire, ops from hugr import tys as ht from hugr.build.dfg import DfBase -from guppylang.ast_util import AstNode, get_type, with_loc, with_type +from guppylang.ast_util import AstNode, get_type, has_empty_body, with_loc, with_type from guppylang.checker.core import Context, Globals from guppylang.checker.expr_checker import check_call, synthesize_call from guppylang.checker.func_checker import check_signature from guppylang.compiler.core import CompiledGlobals, DFContainer from guppylang.definition.common import ParsableDef from guppylang.definition.value import CallReturnWires, CompiledCallableDef +from guppylang.diagnostic import Error, Help from guppylang.error import GuppyError, InternalGuppyError from guppylang.nodes import GlobalCall from guppylang.span import SourceMap @@ -27,6 +29,42 @@ type_to_row, ) +if TYPE_CHECKING: + from guppylang.definition.function import PyFunc + + +@dataclass(frozen=True) +class BodyNotEmptyError(Error): + title: ClassVar[str] = "Unexpected function body" + span_label: ClassVar[str] = "Body of custom function `{name}` must be empty" + name: str + + +@dataclass(frozen=True) +class NoSignatureError(Error): + title: ClassVar[str] = "Type signature missing" + span_label: ClassVar[str] = "Custom function `{name}` requires a type signature" + name: str + + @dataclass(frozen=True) + class Suggestion(Help): + message: ClassVar[str] = ( + "Annotate the type signature of `{name}` or disallow the use of `{name}` " + "as a higher-order value: `@guppy.custom(..., higher_order_value=False)`" + ) + + def __post_init__(self) -> None: + self.add_sub_diagnostic(NoSignatureError.Suggestion(None)) + + +@dataclass(frozen=True) +class NotHigherOrderError(Error): + title: ClassVar[str] = "Not higher-order" + span_label: ClassVar[str] = ( + "Function `{name}` may not be used as a higher-order value" + ) + name: str + @dataclass(frozen=True) class RawCustomFunctionDef(ParsableDef): @@ -47,7 +85,7 @@ class RawCustomFunctionDef(ParsableDef): higher_order_value: Whether the function may be used as a higher-order value. """ - defined_at: ast.FunctionDef + python_func: "PyFunc" call_checker: "CustomCallChecker" call_compiler: "CustomInoutCallCompiler" @@ -69,12 +107,17 @@ def parse(self, globals: "Globals", sources: SourceMap) -> "CustomFunctionDef": code. The only information we need to access is that it's a function type and that there are no unsolved existential vars. """ - sig = self._get_signature(globals) + from guppylang.definition.function import parse_py_func + + func_ast, docstring = parse_py_func(self.python_func, sources) + if not has_empty_body(func_ast): + raise GuppyError(BodyNotEmptyError(func_ast.body[0], self.name)) + sig = self._get_signature(func_ast, globals) ty = sig or FunctionType([], NoneType()) return CustomFunctionDef( self.id, self.name, - self.defined_at, + func_ast, ty, self.call_checker, self.call_compiler, @@ -104,7 +147,9 @@ def compile_call( ) return self.call_compiler.compile_with_inouts(args).regular_returns - def _get_signature(self, globals: Globals) -> FunctionType | None: + def _get_signature( + self, node: ast.FunctionDef, globals: Globals + ) -> FunctionType | None: """Returns the type of the function, if known. Type annotations are needed if we rely on the default call checker or @@ -117,19 +162,15 @@ def _get_signature(self, globals: Globals) -> FunctionType | None: requires_type_annotation = ( isinstance(self.call_checker, DefaultCallChecker) or self.higher_order_value ) - has_type_annotation = self.defined_at.returns or any( - arg.annotation for arg in self.defined_at.args.args + has_type_annotation = node.returns or any( + arg.annotation for arg in node.args.args ) if requires_type_annotation and not has_type_annotation: - raise GuppyError( - f"Type signature for function `{self.name}` is required. " - "Alternatively, try passing `higher_order_value=False` on definition.", - self.defined_at, - ) + raise GuppyError(NoSignatureError(node, self.name)) if requires_type_annotation: - return check_signature(self.defined_at, globals) + return check_signature(node, globals) else: return None @@ -196,10 +237,7 @@ def load_with_args( """ # TODO: This should be raised during checking, not compilation! if not self.higher_order_value: - raise GuppyError( - "This function does not support usage in a higher-order context", - node, - ) + raise GuppyError(NotHigherOrderError(node, self.name)) assert len(self.ty.params) == len(type_args) # We create a `FunctionDef` that takes some inputs, compiles a call to the diff --git a/guppylang/definition/declaration.py b/guppylang/definition/declaration.py index 7a77ac10..c35f3e73 100644 --- a/guppylang/definition/declaration.py +++ b/guppylang/definition/declaration.py @@ -1,5 +1,6 @@ import ast from dataclasses import dataclass, field +from typing import ClassVar from hugr import Node, Wire from hugr import tys as ht @@ -14,6 +15,7 @@ from guppylang.definition.common import CompilableDef, ParsableDef from guppylang.definition.function import PyFunc, parse_py_func from guppylang.definition.value import CallableDef, CallReturnWires, CompiledCallableDef +from guppylang.diagnostic import Error from guppylang.error import GuppyError from guppylang.nodes import GlobalCall from guppylang.span import SourceMap @@ -21,6 +23,13 @@ from guppylang.tys.ty import Type, type_to_row +@dataclass(frozen=True) +class BodyNotEmptyError(Error): + title: ClassVar[str] = "Unexpected function body" + span_label: ClassVar[str] = "Body of declared function `{name}` must be empty" + name: str + + @dataclass(frozen=True) class RawFunctionDecl(ParsableDef): """A raw function declaration provided by the user. @@ -38,9 +47,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "CheckedFunctionDecl": func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature(func_ast, globals.with_python_scope(self.python_scope)) if not has_empty_body(func_ast): - raise GuppyError( - "Body of function declaration must be empty", func_ast.body[0] - ) + raise GuppyError(BodyNotEmptyError(func_ast.body[0], self.name)) return CheckedFunctionDecl( self.id, self.name, diff --git a/guppylang/definition/function.py b/guppylang/definition/function.py index 747db20d..7b2e7cc7 100644 --- a/guppylang/definition/function.py +++ b/guppylang/definition/function.py @@ -14,6 +14,7 @@ from guppylang.ast_util import AstNode, annotate_location, with_loc from guppylang.checker.cfg_checker import CheckedCFG from guppylang.checker.core import Context, Globals, Place, PyScope +from guppylang.checker.errors.generic import ExpectedError, UnsupportedError from guppylang.checker.expr_checker import check_call, synthesize_call from guppylang.checker.func_checker import ( check_global_func_def, @@ -22,7 +23,12 @@ ) from guppylang.compiler.core import CompiledGlobals, DFContainer from guppylang.compiler.func_compiler import compile_global_func_def -from guppylang.definition.common import CheckableDef, CompilableDef, ParsableDef +from guppylang.definition.common import ( + CheckableDef, + CompilableDef, + ParsableDef, + UnknownSourceError, +) from guppylang.definition.value import CallableDef, CallReturnWires, CompiledCallableDef from guppylang.error import GuppyError from guppylang.ipython_inspect import find_ipython_def, is_running_ipython @@ -60,9 +66,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": func_ast, docstring = parse_py_func(self.python_func, sources) ty = check_signature(func_ast, globals.with_python_scope(self.python_scope)) if ty.parametrized: - raise GuppyError( - "Generic function definitions are not supported yet", func_ast - ) + raise GuppyError(UnsupportedError(func_ast, "Generic function definitions")) return ParsedFunctionDef( self.id, self.name, func_ast, ty, self.python_scope, docstring ) @@ -251,9 +255,9 @@ def parse_py_func(f: PyFunc, sources: SourceMap) -> tuple[ast.FunctionDef, str | else: file = inspect.getsourcefile(f) if file is None: - raise GuppyError("Couldn't determine source file for function") + raise GuppyError(UnknownSourceError(None, f)) sources.add_file(file) annotate_location(func_ast, source, file, line_offset) if not isinstance(func_ast, ast.FunctionDef): - raise GuppyError("Expected a function definition", func_ast) + raise GuppyError(ExpectedError(func_ast, "a function definition")) return parse_function_with_docstring(func_ast) diff --git a/guppylang/definition/struct.py b/guppylang/definition/struct.py index f1573322..e20a8cbb 100644 --- a/guppylang/definition/struct.py +++ b/guppylang/definition/struct.py @@ -3,18 +3,24 @@ import textwrap from collections.abc import Sequence from dataclasses import dataclass, replace -from typing import Any +from typing import Any, ClassVar from hugr import Wire, ops from guppylang.ast_util import AstNode, annotate_location from guppylang.checker.core import Globals, PyScope +from guppylang.checker.errors.generic import ( + ExpectedError, + UnexpectedError, + UnsupportedError, +) from guppylang.definition.common import ( CheckableDef, CompiledDef, DefId, Definition, ParsableDef, + UnknownSourceError, ) from guppylang.definition.custom import ( CustomCallCompiler, @@ -23,6 +29,7 @@ ) from guppylang.definition.parameter import ParamDef from guppylang.definition.ty import TypeDef +from guppylang.diagnostic import Error, Help from guppylang.error import GuppyError, InternalGuppyError from guppylang.ipython_inspect import find_ipython_def, is_running_ipython from guppylang.span import SourceMap @@ -48,6 +55,32 @@ class StructField: ty: Type +@dataclass(frozen=True) +class DuplicateFieldError(Error): + title: ClassVar[str] = "Duplicate field" + span_label: ClassVar[str] = ( + "Struct `{struct_name}` already contains a field named `{field_name}`" + ) + struct_name: str + field_name: str + + +@dataclass(frozen=True) +class NonGuppyMethodError(Error): + title: ClassVar[str] = "Not a Guppy method" + span_label: ClassVar[str] = ( + "Method `{method_name}` of struct `{struct_name}` is not a Guppy function" + ) + struct_name: str + method_name: str + + @dataclass(frozen=True) + class Suggestion(Help): + message: ClassVar[str] = ( + "Add a `@guppy` annotation to turn `{method_name}` into a Guppy method" + ) + + @dataclass(frozen=True) class RawStructDef(TypeDef, ParsableDef): """A raw struct type definition that has not been parsed yet.""" @@ -68,7 +101,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedStructDef": """Parses the raw class object into an AST and checks that it is well-formed.""" cls_def = parse_py_class(self.python_class, sources) if cls_def.keywords: - raise GuppyError("Unexpected keyword", cls_def.keywords[0]) + raise GuppyError(UnexpectedError(cls_def.keywords[0], "keyword")) # The only base we allow is `Generic[...]` to specify generic parameters # TODO: This will become obsolete once we have Python 3.12 style generic classes @@ -79,7 +112,10 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedStructDef": case [base] if elems := try_parse_generic_base(base): params = params_from_ast(elems, globals) case bases: - raise GuppyError("Struct inheritance is not supported", bases[0]) + err: Error = UnsupportedError( + bases[0], "Struct inheritance", singular=True + ) + raise GuppyError(err) fields: list[UncheckedStructField] = [] used_field_names: set[str] = set() @@ -96,42 +132,32 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedStructDef": case _, ast.FunctionDef(name=name) as node: v = getattr(self.python_class, name) if not isinstance(v, Definition): - raise GuppyError( - "Add a `@guppy` decorator to this function to add it to " - f"the struct `{self.name}`", - node, - ) + err = NonGuppyMethodError(node, self.name, name) + err.add_sub_diagnostic(NonGuppyMethodError.Suggestion(None)) + raise GuppyError(err) used_func_names[name] = node if name in used_field_names: - raise GuppyError( - f"Struct `{self.name}` already contains a field named " - f"`{name}`", - node, - ) + raise GuppyError(DuplicateFieldError(node, self.name, name)) # Struct fields are declared via annotated assignments without value case _, ast.AnnAssign(target=ast.Name(id=field_name)) as node: if node.value: - raise GuppyError( - "Default struct values are not supported", node.value - ) + err = UnsupportedError(node.value, "Default struct values") + raise GuppyError(err) if field_name in used_field_names: - raise GuppyError( - f"Struct `{self.name}` already contains a field named " - f"`{field_name}`", - node.target, - ) + err = DuplicateFieldError(node.target, self.name, field_name) + raise GuppyError(err) fields.append(UncheckedStructField(field_name, node.annotation)) used_field_names.add(field_name) case _, node: - raise GuppyError("Unexpected statement in struct", node) + err = UnexpectedError( + node, "statement", unexpected_in="struct definition" + ) + raise GuppyError(err) # Ensure that functions don't override struct fields if overridden := used_field_names.intersection(used_func_names.keys()): x = overridden.pop() - raise GuppyError( - f"Struct `{self.name}` already contains a field named `{x}`", - used_func_names[x], - ) + raise GuppyError(DuplicateFieldError(used_func_names[x], self.name, x)) return ParsedStructDef( self.id, self.name, cls_def, params, fields, self.python_scope @@ -245,7 +271,7 @@ def parse_py_class(cls: type, sources: SourceMap) -> ast.ClassDef: if defn is not None: annotate_location(defn.node, defn.cell_source, f"<{defn.cell_name}>", 1) if not isinstance(defn.node, ast.ClassDef): - raise GuppyError("Expected a class definition", defn.node) + raise GuppyError(ExpectedError(defn.node, "a class definition")) return defn.node # else, fall through to handle builtins. source_lines, line_offset = inspect.getsourcelines(cls) @@ -254,12 +280,12 @@ def parse_py_class(cls: type, sources: SourceMap) -> ast.ClassDef: cls_ast = ast.parse(source).body[0] file = inspect.getsourcefile(cls) if file is None: - raise GuppyError("Couldn't determine source file for class") + raise GuppyError(UnknownSourceError(None, cls)) # Store the source file in our cache sources.add_file(file) annotate_location(cls_ast, source, file, line_offset) if not isinstance(cls_ast, ast.ClassDef): - raise GuppyError("Expected a class definition", cls_ast) + raise GuppyError(ExpectedError(cls_ast, "a class definition")) return cls_ast @@ -275,6 +301,13 @@ def try_parse_generic_base(node: ast.expr) -> list[ast.expr] | None: return None +@dataclass(frozen=True) +class RepeatedTypeParamError(Error): + title: ClassVar[str] = "Duplicate type parameter" + span_label: ClassVar[str] = "Type parameter `{name}` cannot be used multiple times" + name: str + + def params_from_ast(nodes: Sequence[ast.expr], globals: Globals) -> list[Parameter]: """Parses a list of AST nodes into unique type parameters. @@ -288,13 +321,11 @@ def params_from_ast(nodes: Sequence[ast.expr], globals: Globals) -> list[Paramet defn = globals[node.id] if isinstance(defn, ParamDef): if defn.id in params_set: - raise GuppyError( - f"Parameter `{node.id}` cannot be used multiple times", node - ) + raise GuppyError(RepeatedTypeParamError(node, node.id)) params.append(defn.to_param(len(params))) params_set.add(defn.id) continue - raise GuppyError("Not a parameter", node) + raise GuppyError(ExpectedError(node, "a type parameter")) return params @@ -319,7 +350,7 @@ def check_instantiate( globals: "Globals", loc: AstNode | None = None, ) -> Type: - raise GuppyError("Recursive structs are not supported", loc) + raise GuppyError(UnsupportedError(loc, "Recursive structs")) dummy_defs = { **globals.defs, diff --git a/guppylang/definition/value.py b/guppylang/definition/value.py index 56ce4380..ba4324e6 100644 --- a/guppylang/definition/value.py +++ b/guppylang/definition/value.py @@ -7,7 +7,6 @@ from guppylang.ast_util import AstNode from guppylang.definition.common import CompiledDef, Definition -from guppylang.error import GuppyError from guppylang.tys.subst import Inst, Subst from guppylang.tys.ty import FunctionType, Type @@ -55,7 +54,7 @@ def synthesize_call( """Synthesizes the return type of a function call.""" def __call__(self, *args: Any, **kwargs: Any) -> Any: - raise GuppyError("Guppy functions can only be called in a Guppy context") + raise RuntimeError("Guppy functions can only be called in a Guppy context") class CompiledCallableDef(CallableDef, CompiledValueDef): diff --git a/guppylang/diagnostic.py b/guppylang/diagnostic.py index c99f5107..8a8ea958 100644 --- a/guppylang/diagnostic.py +++ b/guppylang/diagnostic.py @@ -151,6 +151,15 @@ def rendered_span_label(self) -> str | None: return self._render(self.span_label) +@runtime_checkable +@dataclass(frozen=True) +class Fatal(Diagnostic, Protocol): + """Compiler diagnostic for errors that makes it impossible to proceed, causing an + immediate abort.""" + + level: ClassVar[Literal[DiagnosticLevel.FATAL]] = DiagnosticLevel.FATAL + + @runtime_checkable @dataclass(frozen=True) class Error(Diagnostic, Protocol): diff --git a/tests/error/misc_errors/custom_no_signature.err b/tests/error/misc_errors/custom_no_signature.err new file mode 100644 index 00000000..954a51e5 --- /dev/null +++ b/tests/error/misc_errors/custom_no_signature.err @@ -0,0 +1,11 @@ +Error: Type signature missing (at $FILE:8:0) + | +6 | +7 | @guppy.custom(module=module) +8 | def foo(x): ... + | ^^^^^^^^^^^^^^^ Custom function `foo` requires a type signature + +Help: Annotate the type signature of `foo` or disallow the use of `foo` as a +higher-order value: `@guppy.custom(..., higher_order_value=False)` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/custom_no_signature.py b/tests/error/misc_errors/custom_no_signature.py new file mode 100644 index 00000000..52050c34 --- /dev/null +++ b/tests/error/misc_errors/custom_no_signature.py @@ -0,0 +1,11 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + + +module = GuppyModule("test") + +@guppy.custom(module=module) +def foo(x): ... + + +module.compile() diff --git a/tests/error/misc_errors/custom_not_empty.err b/tests/error/misc_errors/custom_not_empty.err new file mode 100644 index 00000000..6446891b --- /dev/null +++ b/tests/error/misc_errors/custom_not_empty.err @@ -0,0 +1,8 @@ +Error: Unexpected function body (at $FILE:9:4) + | +7 | @guppy.custom(module=module) +8 | def foo(x: int) -> int: +9 | return x + | ^^^^^^^^ Body of custom function `foo` must be empty + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/custom_not_empty.py b/tests/error/misc_errors/custom_not_empty.py new file mode 100644 index 00000000..9cc44fad --- /dev/null +++ b/tests/error/misc_errors/custom_not_empty.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + + +module = GuppyModule("test") + +@guppy.custom(module=module) +def foo(x: int) -> int: + return x + + +module.compile() diff --git a/tests/error/misc_errors/not_a_function_def.err b/tests/error/misc_errors/not_a_function_def.err new file mode 100644 index 00000000..a0f003c4 --- /dev/null +++ b/tests/error/misc_errors/not_a_function_def.err @@ -0,0 +1,8 @@ +Error: Expected a function definition (at $FILE:6:0) + | +4 | module = GuppyModule("test") +5 | +6 | f = lambda x: x + | ^^^^^^^^^^^^^^^ Expected a function definition + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/not_a_function_def.py b/tests/error/misc_errors/not_a_function_def.py new file mode 100644 index 00000000..50f315fb --- /dev/null +++ b/tests/error/misc_errors/not_a_function_def.py @@ -0,0 +1,9 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + +module = GuppyModule("test") + +f = lambda x: x +guppy(module)(f) + +module.compile() diff --git a/tests/error/misc_errors/not_higher_order.err b/tests/error/misc_errors/not_higher_order.err new file mode 100644 index 00000000..40ae0ad7 --- /dev/null +++ b/tests/error/misc_errors/not_higher_order.err @@ -0,0 +1,8 @@ +Error: Not higher-order (at $FILE:6:8) + | +4 | @compile_guppy +5 | def foo() -> None: +6 | f = len + | ^^^ Function `len` may not be used as a higher-order value + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/not_higher_order.py b/tests/error/misc_errors/not_higher_order.py new file mode 100644 index 00000000..4ec047b5 --- /dev/null +++ b/tests/error/misc_errors/not_higher_order.py @@ -0,0 +1,6 @@ +from tests.util import compile_guppy + + +@compile_guppy +def foo() -> None: + f = len diff --git a/tests/error/misc_errors/unexpected_body.err b/tests/error/misc_errors/unexpected_body.err new file mode 100644 index 00000000..bcf42d8b --- /dev/null +++ b/tests/error/misc_errors/unexpected_body.err @@ -0,0 +1,8 @@ +Error: Unexpected function body (at $FILE:9:4) + | +7 | @guppy.declare(module) +8 | def foo() -> int: +9 | return 42 + | ^^^^^^^^^ Body of declared function `foo` must be empty + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unexpected_body.py b/tests/error/misc_errors/unexpected_body.py new file mode 100644 index 00000000..1aed92fb --- /dev/null +++ b/tests/error/misc_errors/unexpected_body.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + + +module = GuppyModule("test") + +@guppy.declare(module) +def foo() -> int: + return 42 + + +module.compile() diff --git a/tests/error/poly_errors/define.err b/tests/error/poly_errors/define.err index d2500379..4fef9e69 100644 --- a/tests/error/poly_errors/define.err +++ b/tests/error/poly_errors/define.err @@ -1,6 +1,10 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Unsupported (at $FILE:11:0) + | + 9 | +10 | @guppy(module) +11 | def main(x: T) -> T: + | ^^^^^^^^^^^^^^^^^^^^ +12 | return x + | ^^^^^^^^^^^^ Generic function definitions are not supported -9: @guppy(module) -10: def main(x: T) -> T: - ^^^^^^^^^^^^^^^^^^^^ -GuppyError: Generic function definitions are not supported yet +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/default.err b/tests/error/struct_errors/default.err index 62d6dcf0..e66e3808 100644 --- a/tests/error/struct_errors/default.err +++ b/tests/error/struct_errors/default.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Unsupported (at $FILE:10:13) + | + 8 | @guppy.struct(module) + 9 | class MyStruct: +10 | x: int = 42 + | ^^ Default struct values are not supported -8: @guppy.struct(module) -9: class MyStruct: -10: x: int = 42 - ^^ -GuppyError: Default struct values are not supported +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/duplicate_field.err b/tests/error/struct_errors/duplicate_field.err index 7058083c..03f7771f 100644 --- a/tests/error/struct_errors/duplicate_field.err +++ b/tests/error/struct_errors/duplicate_field.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Duplicate field (at $FILE:11:4) + | + 9 | class MyStruct: +10 | x: int +11 | x: bool + | ^ Struct `MyStruct` already contains a field named `x` -9: class MyStruct: -10: x: int -11: x: bool - ^ -GuppyError: Struct `MyStruct` already contains a field named `x` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/func_overrides_field1.err b/tests/error/struct_errors/func_overrides_field1.err index 9dd60e4c..f150e008 100644 --- a/tests/error/struct_errors/func_overrides_field1.err +++ b/tests/error/struct_errors/func_overrides_field1.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Duplicate field (at $FILE:13:4) + | +11 | +12 | @guppy(module) +13 | def x(self: "MyStruct") -> int: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +14 | return 0 + | ^^^^^^^^^^^^^^^^ Struct `MyStruct` already contains a field named `x` -11: -12: @guppy(module) -13: def x(self: "MyStruct") -> int: - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -GuppyError: Struct `MyStruct` already contains a field named `x` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/func_overrides_field2.err b/tests/error/struct_errors/func_overrides_field2.err index bada6583..a5badd59 100644 --- a/tests/error/struct_errors/func_overrides_field2.err +++ b/tests/error/struct_errors/func_overrides_field2.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Duplicate field (at $FILE:11:4) + | + 9 | class MyStruct: +10 | @guppy(module) +11 | def x(self: "MyStruct") -> int: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +12 | return 0 + | ^^^^^^^^^^^^^^^^ Struct `MyStruct` already contains a field named `x` -9: class MyStruct: -10: @guppy(module) -11: def x(self: "MyStruct") -> int: - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -GuppyError: Struct `MyStruct` already contains a field named `x` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/inheritance.err b/tests/error/struct_errors/inheritance.err index 5f76b29b..57410d4b 100644 --- a/tests/error/struct_errors/inheritance.err +++ b/tests/error/struct_errors/inheritance.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Unsupported (at $FILE:9:15) + | +7 | +8 | @guppy.struct(module) +9 | class MyStruct(int): + | ^^^ Struct inheritance is not supported -7: @guppy.struct(module) -8: class MyStruct(int): - ^^^ -GuppyError: Struct inheritance is not supported +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/invalid_generic.err b/tests/error/struct_errors/invalid_generic.err index 975a726e..4f564b74 100644 --- a/tests/error/struct_errors/invalid_generic.err +++ b/tests/error/struct_errors/invalid_generic.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Expected a type parameter (at $FILE:14:23) + | +12 | +13 | @guppy.struct(module) +14 | class MyStruct(Generic[X]): + | ^ Expected a type parameter -12: @guppy.struct(module) -13: class MyStruct(Generic[X]): - ^ -GuppyError: Not a parameter +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/keywords.err b/tests/error/struct_errors/keywords.err index 2abaeb4e..225b4b13 100644 --- a/tests/error/struct_errors/keywords.err +++ b/tests/error/struct_errors/keywords.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Unexpected keyword (at $FILE:9:15) + | +7 | +8 | @guppy.struct(module) +9 | class MyStruct(metaclass=type): + | ^^^^^^^^^^^^^^ Unexpected keyword -7: @guppy.struct(module) -8: class MyStruct(metaclass=type): - ^^^^^^^^^^^^^^ -GuppyError: Unexpected keyword +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/mutual_recursive.err b/tests/error/struct_errors/mutual_recursive.err index dccc76ef..86ffccda 100644 --- a/tests/error/struct_errors/mutual_recursive.err +++ b/tests/error/struct_errors/mutual_recursive.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:15 +Error: Unsupported (at $FILE:15:8) + | +13 | @guppy.struct(module) +14 | class StructB: +15 | y: "StructA" + | ^^^^^^^ Recursive structs are not supported -13: @guppy.struct(module) -14: class StructB: -15: y: "StructA" - ^^^^^^^ -GuppyError: Recursive structs are not supported +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/non_guppy_func.err b/tests/error/struct_errors/non_guppy_func.err index 65770dce..821f4d26 100644 --- a/tests/error/struct_errors/non_guppy_func.err +++ b/tests/error/struct_errors/non_guppy_func.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Not a Guppy method (at $FILE:12:4) + | +10 | x: int +11 | +12 | def f(self: "MyStruct") -> None: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +13 | pass + | ^^^^^^^^^^^^ Method `f` of struct `MyStruct` is not a Guppy function -10: x: int -11: -12: def f(self: "MyStruct") -> None: - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -GuppyError: Add a `@guppy` decorator to this function to add it to the struct `MyStruct` +Help: Add a `@guppy` annotation to turn `f` into a Guppy method + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/not_a_class_def.err b/tests/error/struct_errors/not_a_class_def.err new file mode 100644 index 00000000..184804d7 --- /dev/null +++ b/tests/error/struct_errors/not_a_class_def.err @@ -0,0 +1,10 @@ +Error: Expected a class definition (at $FILE:8:0) + | +6 | +7 | @guppy.struct(module) +8 | def foo(x: int) -> int: + | ^^^^^^^^^^^^^^^^^^^^^^^ +9 | return x + | ^^^^^^^^^^^^ Expected a class definition + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/not_a_class_def.py b/tests/error/struct_errors/not_a_class_def.py new file mode 100644 index 00000000..f9ab60a3 --- /dev/null +++ b/tests/error/struct_errors/not_a_class_def.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + +module = GuppyModule("test") + + +@guppy.struct(module) +def foo(x: int) -> int: + return x + + +module.compile() diff --git a/tests/error/struct_errors/recursive.err b/tests/error/struct_errors/recursive.err index 1be8dfe0..4bbe889c 100644 --- a/tests/error/struct_errors/recursive.err +++ b/tests/error/struct_errors/recursive.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Unsupported (at $FILE:10:14) + | + 8 | @guppy.struct(module) + 9 | class MyStruct: +10 | x: "tuple[MyStruct, int]" + | ^^^^^^^^ Recursive structs are not supported -8: @guppy.struct(module) -9: class MyStruct: -10: x: "tuple[MyStruct, int]" - ^^^^^^^^ -GuppyError: Recursive structs are not supported +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/repeated_generic.err b/tests/error/struct_errors/repeated_generic.err new file mode 100644 index 00000000..3793374d --- /dev/null +++ b/tests/error/struct_errors/repeated_generic.err @@ -0,0 +1,8 @@ +Error: Duplicate type parameter (at $FILE:17:26) + | +15 | +16 | @guppy.struct(module) +17 | class MyStruct(Generic[X, X]): + | ^ Type parameter `X` cannot be used multiple times + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/repeated_generic.py b/tests/error/struct_errors/repeated_generic.py new file mode 100644 index 00000000..2d111265 --- /dev/null +++ b/tests/error/struct_errors/repeated_generic.py @@ -0,0 +1,21 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + + +module = GuppyModule("test") + +X = guppy.type_var("X", module=module) + + +class Generic: + """Fake Generic type that doesn't check for type var uniqueness.""" + def __class_getitem__(cls, item): + return cls + + +@guppy.struct(module) +class MyStruct(Generic[X, X]): + x: int + + +module.compile() diff --git a/tests/error/struct_errors/stray_docstring.err b/tests/error/struct_errors/stray_docstring.err index 368aae82..c8dc3270 100644 --- a/tests/error/struct_errors/stray_docstring.err +++ b/tests/error/struct_errors/stray_docstring.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Unexpected statement (at $FILE:11:4) + | + 9 | class MyStruct: +10 | x: int +11 | """Docstring in wrong position""" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unexpected statement in struct definition -9: class MyStruct: -10: x: int -11: """Docstring in wrong position""" - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -GuppyError: Unexpected statement in struct +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/type_missing1.err b/tests/error/struct_errors/type_missing1.err index afff66af..35139780 100644 --- a/tests/error/struct_errors/type_missing1.err +++ b/tests/error/struct_errors/type_missing1.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Unexpected statement (at $FILE:13:4) + | +11 | @guppy.struct(module) +12 | class MyStruct: +13 | x + | ^ Unexpected statement in struct definition -11: @guppy.struct(module) -12: class MyStruct: -13: x - ^ -GuppyError: Unexpected statement in struct +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/type_missing2.err b/tests/error/struct_errors/type_missing2.err index f70c330b..be74a14e 100644 --- a/tests/error/struct_errors/type_missing2.err +++ b/tests/error/struct_errors/type_missing2.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Unexpected statement (at $FILE:10:4) + | + 8 | @guppy.struct(module) + 9 | class MyStruct: +10 | x = 42 + | ^^^^^^ Unexpected statement in struct definition -8: @guppy.struct(module) -9: class MyStruct: -10: x = 42 - ^^^^^^ -GuppyError: Unexpected statement in struct +Guppy compilation failed due to 1 previous error From c4a2aca99dba50aa0aa8c767edcd9124860f8ee2 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:56:09 +0000 Subject: [PATCH 16/20] feat: Update type parsing to use new diagnostics (#605) --- guppylang/decorator.py | 7 +- guppylang/tys/builtin.py | 5 +- guppylang/tys/errors.py | 136 ++++++++++++++++++ guppylang/tys/param.py | 12 +- guppylang/tys/parsing.py | 84 +++++------ tests/error/inout_errors/nonlinear.err | 12 +- .../error/inout_errors/nonlinear_callable.err | 12 +- tests/error/misc_errors/callable_no_args.err | 15 +- .../error/misc_errors/callable_not_list1.err | 15 +- .../error/misc_errors/callable_not_list2.err | 15 +- tests/error/misc_errors/extern_bad_type.err | 13 +- tests/error/misc_errors/invalid_arg_flag.err | 12 +- tests/error/misc_errors/negative_nat_arg.err | 12 +- tests/error/misc_errors/nested_arg_flag.err | 12 +- tests/error/misc_errors/return_flag.err | 12 +- .../misc_errors/return_flag_callable.err | 12 +- .../misc_errors/type_attr_not_module.err | 12 +- .../error/misc_errors/type_attr_undefined.err | 12 +- tests/error/py_errors/invalid_type_arg.err | 13 +- .../struct_errors/invalid_instantiate1.err | 13 +- .../struct_errors/invalid_instantiate2.err | 13 +- 21 files changed, 310 insertions(+), 139 deletions(-) create mode 100644 guppylang/tys/errors.py diff --git a/guppylang/decorator.py b/guppylang/decorator.py index 857cb443..09bd85ca 100644 --- a/guppylang/decorator.py +++ b/guppylang/decorator.py @@ -486,11 +486,12 @@ def _parse_expr_string(ty_str: str, parse_err: str, sources: SourceMap) -> ast.e sources.add_file(info.filename) source_lines, _ = inspect.getsourcelines(caller_module) source = "".join(source_lines) - annotate_location(expr_ast, source, info.filename, 0) + annotate_location(expr_ast, source, info.filename, 1) # Modify the AST so that all sub-nodes span the entire line. We # can't give a better location since we don't know the column # offset of the `ty` argument for node in [expr_ast, *ast.walk(expr_ast)]: - node.lineno, node.col_offset = info.lineno, 0 - node.end_col_offset = len(source_lines[info.lineno - 1]) + node.lineno = node.end_lineno = info.lineno + node.col_offset = 0 + node.end_col_offset = len(source_lines[info.lineno - 1]) - 1 return expr_ast diff --git a/guppylang/tys/builtin.py b/guppylang/tys/builtin.py index 1634ec14..4da8a5a4 100644 --- a/guppylang/tys/builtin.py +++ b/guppylang/tys/builtin.py @@ -13,6 +13,7 @@ from guppylang.experimental import check_lists_enabled from guppylang.tys.arg import Argument, ConstArg, TypeArg from guppylang.tys.const import ConstValue +from guppylang.tys.errors import WrongNumberOfTypeArgsError from guppylang.tys.param import ConstParam, TypeParam from guppylang.tys.ty import ( FunctionType, @@ -78,7 +79,7 @@ def check_instantiate( self, args: Sequence[Argument], globals: "Globals", loc: AstNode | None = None ) -> NoneType: if args: - raise GuppyError("Type `None` is not parameterized", loc) + raise GuppyError(WrongNumberOfTypeArgsError(loc, 0, len(args), "None")) return NoneType() @@ -95,7 +96,7 @@ def check_instantiate( self, args: Sequence[Argument], globals: "Globals", loc: AstNode | None = None ) -> NumericType: if args: - raise GuppyError(f"Type `{self.name}` is not parameterized", loc) + raise GuppyError(WrongNumberOfTypeArgsError(loc, 0, len(args), self.name)) return self.ty diff --git a/guppylang/tys/errors.py b/guppylang/tys/errors.py new file mode 100644 index 00000000..3005378d --- /dev/null +++ b/guppylang/tys/errors.py @@ -0,0 +1,136 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING, ClassVar + +from guppylang.diagnostic import Error, Help, Note + +if TYPE_CHECKING: + from guppylang.definition.parameter import ParamDef + from guppylang.tys.ty import Type + + +@dataclass(frozen=True) +class WrongNumberOfTypeArgsError(Error): + title: ClassVar[str] = "" # Custom implementation in `rendered_title` + expected: int + actual: int + type_name: str + + @property + def rendered_title(self) -> str: + if self.expected == 0: + return "Non-parametric type" + elif self.expected > self.actual: + return "Missing type arguments" + else: + return "Too many type arguments" + + @property + def rendered_span_label(self) -> str: + if self.expected == 0: + return f"Type `{self.type_name}` is not parametric" + diff = self.expected - self.actual + msg = "Unexpected " if diff < 0 else "Missing " + msg += "type arguments " if abs(diff) > 1 else "type argument " + msg += ( + f"for type `{self.type_name}` (expected {self.expected}, got {self.actual})" + ) + return msg + + +@dataclass(frozen=True) +class InvalidTypeArgError(Error): + title: ClassVar[str] = "Invalid type argument" + span_label: ClassVar[str] = "Not a valid type argument" + + +@dataclass(frozen=True) +class IllegalPyTypeArgError(Error): + title: ClassVar[str] = "Invalid type argument" + span_label: ClassVar[str] = ( + "Compile-time `py(...)` expression evaluating to `{obj}` is not a valid type " + "argument" + ) + obj: object + + +@dataclass(frozen=True) +class ModuleMemberNotFoundError(Error): + # TODO: Unify with the definition in expression checker once merged + title: ClassVar[str] = "Not found in module" + span_label: ClassVar[str] = "Module `{module_name}` has no member `{member}`" + module_name: str + member: str + + +@dataclass(frozen=True) +class HigherKindedTypeVarError(Error): + title: ClassVar[str] = "Not parametric" + span_label: ClassVar[str] = ( + "Type variable `{var_def.name}` doesn't take type arguments" + ) + var_def: "ParamDef" + + @dataclass(frozen=True) + class Explain(Note): + message: ClassVar[str] = "Higher-kinded types are not supported" + + def __post_init__(self) -> None: + self.add_sub_diagnostic(HigherKindedTypeVarError.Explain(None)) + + +@dataclass(frozen=True) +class FreeTypeVarError(Error): + title: ClassVar[str] = "Free type variable" + span_label: ClassVar[str] = "Type variable `{var_def.name}` is unbound" + var_def: "ParamDef" + + @dataclass(frozen=True) + class Explain(Note): + message: ClassVar[str] = ( + "Only struct and function definitions can be generic. Other generic values " + "or nested types are not supported." + ) + + def __post_init__(self) -> None: + self.add_sub_diagnostic(FreeTypeVarError.Explain(None)) + + +@dataclass(frozen=True) +class InvalidTypeError(Error): + title: ClassVar[str] = "Invalid type" + span_label: ClassVar[str] = "Not a valid type" + + +@dataclass(frozen=True) +class InvalidCallableTypeError(Error): + title: ClassVar[str] = "Invalid type" + span_label: ClassVar[str] = "Invalid function type" + + @dataclass(frozen=True) + class Explain(Help): + message: ClassVar[str] = ( + "Function types are specified as follows: " + "`Callable[[], ]`" + ) + + def __post_init__(self) -> None: + self.add_sub_diagnostic(InvalidCallableTypeError.Explain(None)) + + +@dataclass(frozen=True) +class NonLinearOwnedError(Error): + title: ClassVar[str] = "Invalid annotation" + span_label: ClassVar[str] = "Classical type `{ty}` cannot be owned" + ty: "Type" + + +@dataclass(frozen=True) +class InvalidFlagError(Error): + title: ClassVar[str] = "Invalid annotation" + span_label: ClassVar[str] = "Invalid type annotation" + + +@dataclass(frozen=True) +class FlagNotAllowedError(Error): + title: ClassVar[str] = "Invalid annotation" + span_label: ClassVar[str] = "`@` type annotations are not allowed in this position" diff --git a/guppylang/tys/param.py b/guppylang/tys/param.py index ceee77e6..f5186ba6 100644 --- a/guppylang/tys/param.py +++ b/guppylang/tys/param.py @@ -11,6 +11,7 @@ from guppylang.tys.arg import Argument, ConstArg, TypeArg from guppylang.tys.common import ToHugr from guppylang.tys.const import BoundConstVar, ExistentialConstVar +from guppylang.tys.errors import WrongNumberOfTypeArgsError from guppylang.tys.var import ExistentialVar if TYPE_CHECKING: @@ -201,12 +202,11 @@ def check_all_args( invalid. """ exp, act = len(params), len(args) - if exp > act: - raise GuppyError(f"Missing parameter for type `{type_name}`", loc) - elif 0 == exp < act: - raise GuppyError(f"Type `{type_name}` is not parameterized", loc) - elif 0 < exp < act: - raise GuppyError(f"Too many parameters for type `{type_name}`", loc) + if exp != act: + # TODO: Adjust the error span to only point to the offending arguments (similar + # to how we deal with call args in the expression checker). This requires + # threading the type arg spans down to this point + raise GuppyError(WrongNumberOfTypeArgsError(loc, exp, act, type_name)) # Now check that the kinds match up for param, arg in zip(params, args, strict=True): diff --git a/guppylang/tys/parsing.py b/guppylang/tys/parsing.py index 56985f47..c17e76d3 100644 --- a/guppylang/tys/parsing.py +++ b/guppylang/tys/parsing.py @@ -8,6 +8,7 @@ ) from guppylang.cfg.builder import is_py_expression from guppylang.checker.core import Context, Globals, Locals +from guppylang.checker.errors.generic import ExpectedError from guppylang.checker.expr_checker import eval_py_expr from guppylang.definition.common import Definition from guppylang.definition.module import ModuleDef @@ -17,6 +18,18 @@ from guppylang.tys.arg import Argument, ConstArg, TypeArg from guppylang.tys.builtin import CallableTypeDef from guppylang.tys.const import ConstValue +from guppylang.tys.errors import ( + FlagNotAllowedError, + FreeTypeVarError, + HigherKindedTypeVarError, + IllegalPyTypeArgError, + InvalidCallableTypeError, + InvalidFlagError, + InvalidTypeArgError, + InvalidTypeError, + ModuleMemberNotFoundError, + NonLinearOwnedError, +) from guppylang.tys.param import Parameter, TypeParam from guppylang.tys.ty import ( FuncInput, @@ -78,41 +91,38 @@ def arg_from_ast( nat_ty = NumericType(NumericType.Kind.Nat) return ConstArg(ConstValue(nat_ty, v)) else: - raise GuppyError( - f"Compile-time `py(...)` expression with type `{type(v)}` is not a " - "valid type argument", - node, - ) + raise GuppyError(IllegalPyTypeArgError(node, v)) # Finally, we also support delayed annotations in strings if isinstance(node, ast.Constant) and isinstance(node.value, str): node = _parse_delayed_annotation(node.value, node) return arg_from_ast(node, globals, param_var_mapping) - raise GuppyError("Not a valid type argument", node) + raise GuppyError(InvalidTypeArgError(node)) def _try_parse_defn(node: AstNode, globals: Globals) -> Definition | None: """Tries to parse a (possibly qualified) name into a global definition.""" + from guppylang.checker.cfg_checker import VarNotDefinedError + match node: case ast.Name(id=x): if x not in globals: - raise GuppyError(f"Unknown identifier: `{x}`", node) + raise GuppyError(VarNotDefinedError(node, x)) return globals[x] case ast.Attribute(value=ast.Name(id=module_name) as value, attr=x): if module_name not in globals: - raise GuppyError(f"Unknown identifier: `{module_name}`", value) + raise GuppyError(VarNotDefinedError(value, module_name)) module_def = globals[module_name] if not isinstance(module_def, ModuleDef): - raise GuppyError( - f"Expected a module, got {module_def.description} " - f"`{module_def.name}`", + err = ExpectedError( value, + "a module", + got=f"{module_def.description} `{module_def.name}`", ) + raise GuppyError(err) if x not in module_def.globals: - raise GuppyError( - f"Module `{module_def.name}` has no member `{x}`", node - ) + raise GuppyError(ModuleMemberNotFoundError(node, module_def.name, x)) return module_def.globals[x] case _: return None @@ -144,22 +154,15 @@ def _arg_from_instantiated_defn( case ParamDef() as defn: # We don't allow parametrised variables like `T[int]` if arg_nodes: - raise GuppyError( - f"Variable `{defn.name}` is not parameterized. Higher-kinded types " - f"are not supported", - node, - ) + raise GuppyError(HigherKindedTypeVarError(node, defn)) if param_var_mapping is None: - raise GuppyError( - "Free type variable. Only function types can be generic", node - ) + raise GuppyError(FreeTypeVarError(node, defn)) if defn.name not in param_var_mapping: param_var_mapping[defn.name] = defn.to_param(len(param_var_mapping)) return param_var_mapping[defn.name].to_bound() case defn: - raise GuppyError( - f"Expected a type, got {defn.description} `{defn.name}`", node - ) + err = ExpectedError(node, "a type", got=f"{defn.description} `{defn.name}`") + raise GuppyError(err) def _parse_delayed_annotation(ast_str: str, node: ast.Constant) -> ast.expr: @@ -167,7 +170,7 @@ def _parse_delayed_annotation(ast_str: str, node: ast.Constant) -> ast.expr: try: [stmt] = ast.parse(ast_str).body if not isinstance(stmt, ast.Expr): - raise GuppyError("Invalid Guppy type", node) + raise GuppyError(InvalidTypeError(node)) set_location_from(stmt, loc=node) shift_loc( stmt, @@ -175,7 +178,7 @@ def _parse_delayed_annotation(ast_str: str, node: ast.Constant) -> ast.expr: delta_col_offset=node.col_offset + 1, # +1 to remove the `"` ) except (SyntaxError, ValueError): - raise GuppyError("Invalid Guppy type", node) from None + raise GuppyError(InvalidTypeError(node)) from None else: return stmt.value @@ -187,15 +190,12 @@ def _parse_callable_type( param_var_mapping: dict[str, Parameter] | None, ) -> FunctionType: """Helper function to parse a `Callable[[], ]` type.""" - err = ( - "Function types should be specified via " - "`Callable[[], ]`" - ) + err = InvalidCallableTypeError(loc) if len(args) != 2: - raise GuppyError(err, loc) + raise GuppyError(err) [inputs, output] = args if not isinstance(inputs, ast.List): - raise GuppyError(err, loc) + raise GuppyError(err) inouts, output = parse_function_io_types( inputs.elts, output, loc, globals, param_var_mapping ) @@ -219,9 +219,7 @@ def parse_function_io_types( for inp in input_nodes: ty, flags = type_with_flags_from_ast(inp, globals, param_var_mapping) if InputFlags.Owned in flags and not ty.linear: - raise GuppyError( - f"Non-linear type `{ty}` cannot be annotated as `@owned`", loc - ) + raise GuppyError(NonLinearOwnedError(loc, ty)) if ty.linear and InputFlags.Owned not in flags: flags |= InputFlags.Inout @@ -243,15 +241,10 @@ def type_with_flags_from_ast( match node.right: case ast.Name(id="owned"): if not ty.linear: - raise GuppyError( - f"Non-linear type `{ty}` cannot be annotated as `@owned`", - node.right, - ) + raise GuppyError(NonLinearOwnedError(node.right, ty)) flags |= InputFlags.Owned - case ast.Name(name): - raise GuppyError(f"Invalid annotation: `{name}`", node.right) case _: - raise GuppyError("Invalid annotation", node.right) + raise GuppyError(InvalidFlagError(node.right)) return ty, flags # We also need to handle the case that this could be a delayed string annotation elif isinstance(node, ast.Constant) and isinstance(node.value, str): @@ -273,10 +266,7 @@ def type_from_ast( ty, flags = type_with_flags_from_ast(node, globals, param_var_mapping) if flags != InputFlags.NoFlags: assert InputFlags.Inout not in flags # Users shouldn't be able to set this - raise GuppyError( - "`@` type annotations are not allowed in this position", - node, - ) + raise GuppyError(FlagNotAllowedError(node)) return ty diff --git a/tests/error/inout_errors/nonlinear.err b/tests/error/inout_errors/nonlinear.err index db035a03..6c92b754 100644 --- a/tests/error/inout_errors/nonlinear.err +++ b/tests/error/inout_errors/nonlinear.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Invalid annotation (at $FILE:11:16) + | + 9 | +10 | @guppy.declare(module) +11 | def foo(x: int @owned) -> qubit: ... + | ^^^^^ Classical type `int` cannot be owned -9: @guppy.declare(module) -10: def foo(x: int @owned) -> qubit: ... - ^^^^^ -GuppyError: Non-linear type `int` cannot be annotated as `@owned` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/inout_errors/nonlinear_callable.err b/tests/error/inout_errors/nonlinear_callable.err index 764445cb..0ca13b6c 100644 --- a/tests/error/inout_errors/nonlinear_callable.err +++ b/tests/error/inout_errors/nonlinear_callable.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Invalid annotation (at $FILE:12:26) + | +10 | +11 | @guppy.declare(module) +12 | def foo(f: Callable[[int @owned], None]) -> None: ... + | ^^^^^ Classical type `int` cannot be owned -10: @guppy.declare(module) -11: def foo(f: Callable[[int @owned], None]) -> None: ... - ^^^^^ -GuppyError: Non-linear type `int` cannot be annotated as `@owned` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/callable_no_args.err b/tests/error/misc_errors/callable_no_args.err index f0be8013..a514ccca 100644 --- a/tests/error/misc_errors/callable_no_args.err +++ b/tests/error/misc_errors/callable_no_args.err @@ -1,6 +1,11 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Invalid type (at $FILE:10:11) + | + 8 | + 9 | @guppy.declare(module) +10 | def foo(f: Callable) -> None: ... + | ^^^^^^^^ Invalid function type -8: @guppy.declare(module) -9: def foo(f: Callable) -> None: ... - ^^^^^^^^ -GuppyError: Function types should be specified via `Callable[[], ]` +Help: Function types are specified as follows: `Callable[[], ]` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/callable_not_list1.err b/tests/error/misc_errors/callable_not_list1.err index 27599706..5e500b23 100644 --- a/tests/error/misc_errors/callable_not_list1.err +++ b/tests/error/misc_errors/callable_not_list1.err @@ -1,6 +1,11 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Invalid type (at $FILE:10:12) + | + 8 | + 9 | @guppy.declare(module) +10 | def foo(f: "Callable[int, float, bool]") -> None: ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid function type -8: @guppy.declare(module) -9: def foo(f: "Callable[int, float, bool]") -> None: ... - ^^^^^^^^^^^^^^^^^^^^^^^^^^ -GuppyError: Function types should be specified via `Callable[[], ]` +Help: Function types are specified as follows: `Callable[[], ]` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/callable_not_list2.err b/tests/error/misc_errors/callable_not_list2.err index 6a58a302..88ade629 100644 --- a/tests/error/misc_errors/callable_not_list2.err +++ b/tests/error/misc_errors/callable_not_list2.err @@ -1,6 +1,11 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Invalid type (at $FILE:10:12) + | + 8 | + 9 | @guppy.declare(module) +10 | def foo(f: "Callable[None]") -> None: ... + | ^^^^^^^^^^^^^^ Invalid function type -8: @guppy.declare(module) -9: def foo(f: "Callable[None]") -> None: ... - ^^^^^^^^^^^^^^ -GuppyError: Function types should be specified via `Callable[[], ]` +Help: Function types are specified as follows: `Callable[[], ]` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/extern_bad_type.err b/tests/error/misc_errors/extern_bad_type.err index 1d4d85b7..412fc313 100644 --- a/tests/error/misc_errors/extern_bad_type.err +++ b/tests/error/misc_errors/extern_bad_type.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Non-parametric type (at $FILE:10:0) + | + 8 | module = GuppyModule("test") + 9 | +10 | guppy.extern("x", ty="float[int]", module=module) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Type `float` is not parametric -7: module = GuppyModule("test") -8: -9: guppy.extern("x", ty="float[int]", module=module) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -GuppyError: Type `float` is not parameterized +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/invalid_arg_flag.err b/tests/error/misc_errors/invalid_arg_flag.err index 419780ca..58b63855 100644 --- a/tests/error/misc_errors/invalid_arg_flag.err +++ b/tests/error/misc_errors/invalid_arg_flag.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Invalid annotation (at $FILE:12:16) + | +10 | +11 | @guppy.declare(module) +12 | def foo(x: int @blah) -> qubit: ... + | ^^^^ Invalid type annotation -10: @guppy.declare(module) -11: def foo(x: int @blah) -> qubit: ... - ^^^^ -GuppyError: Invalid annotation: `blah` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/negative_nat_arg.err b/tests/error/misc_errors/negative_nat_arg.err index 94e3b222..4bf74bc6 100644 --- a/tests/error/misc_errors/negative_nat_arg.err +++ b/tests/error/misc_errors/negative_nat_arg.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Invalid type argument (at $FILE:6:22) + | +4 | +5 | @compile_guppy +6 | def foo(x: array[int, -5]) -> None: + | ^^ Not a valid type argument -4: @compile_guppy -5: def foo(x: array[int, -5]) -> None: - ^^ -GuppyError: Not a valid type argument +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/nested_arg_flag.err b/tests/error/misc_errors/nested_arg_flag.err index b415831d..4fe1723a 100644 --- a/tests/error/misc_errors/nested_arg_flag.err +++ b/tests/error/misc_errors/nested_arg_flag.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Invalid type argument (at $FILE:12:16) + | +10 | +11 | @guppy.declare(module) +12 | def foo(x: list[qubit @owned]) -> qubit: ... + | ^^^^^^^^^^^^ Not a valid type argument -10: @guppy.declare(module) -11: def foo(x: list[qubit @owned]) -> qubit: ... - ^^^^^^^^^^^^ -GuppyError: Not a valid type argument +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/return_flag.err b/tests/error/misc_errors/return_flag.err index 45982137..456a05e6 100644 --- a/tests/error/misc_errors/return_flag.err +++ b/tests/error/misc_errors/return_flag.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:12 +Error: Invalid annotation (at $FILE:12:13) + | +10 | +11 | @guppy.declare(module) +12 | def foo() -> qubit @owned: ... + | ^^^^^^^^^^^^ `@` type annotations are not allowed in this position -10: @guppy.declare(module) -11: def foo() -> qubit @owned: ... - ^^^^^^^^^^^^ -GuppyError: `@` type annotations are not allowed in this position +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/return_flag_callable.err b/tests/error/misc_errors/return_flag_callable.err index 348558ee..166a2e6f 100644 --- a/tests/error/misc_errors/return_flag_callable.err +++ b/tests/error/misc_errors/return_flag_callable.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Invalid annotation (at $FILE:14:25) + | +12 | +13 | @guppy.declare(module) +14 | def foo(f: "Callable[[], qubit @owned]") -> None: ... + | ^^^^^^^^^^^^ `@` type annotations are not allowed in this position -12: @guppy.declare(module) -13: def foo(f: "Callable[[], qubit @owned]") -> None: ... - ^^^^^^^^^^^^ -GuppyError: `@` type annotations are not allowed in this position +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/type_attr_not_module.err b/tests/error/misc_errors/type_attr_not_module.err index da24a4ce..4aa91966 100644 --- a/tests/error/misc_errors/type_attr_not_module.err +++ b/tests/error/misc_errors/type_attr_not_module.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:5 +Error: Expected a module (at $FILE:5:10) + | +3 | +4 | @compile_guppy +5 | def f(x: "int.bar") -> None: + | ^^^ Expected a module, got type `int` -3: @compile_guppy -4: def f(x: "int.bar") -> None: - ^^^ -GuppyError: Expected a module, got type `int` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/type_attr_undefined.err b/tests/error/misc_errors/type_attr_undefined.err index 4d3ebfa8..7fe50f03 100644 --- a/tests/error/misc_errors/type_attr_undefined.err +++ b/tests/error/misc_errors/type_attr_undefined.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:5 +Error: Variable not defined (at $FILE:5:10) + | +3 | +4 | @compile_guppy +5 | def f(x: "foo.bar") -> None: + | ^^^ `foo` is not defined -3: @compile_guppy -4: def f(x: "foo.bar") -> None: - ^^^ -GuppyError: Unknown identifier: `foo` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/invalid_type_arg.err b/tests/error/py_errors/invalid_type_arg.err index a310bd6b..e89d3745 100644 --- a/tests/error/py_errors/invalid_type_arg.err +++ b/tests/error/py_errors/invalid_type_arg.err @@ -1,6 +1,9 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Invalid type argument (at $FILE:7:23) + | +5 | +6 | @compile_guppy +7 | def foo(xs: array[int, py(1.0)]) -> None: + | ^^^^^^^ Compile-time `py(...)` expression evaluating to `1.0` is + | not a valid type argument -5: @compile_guppy -6: def foo(xs: array[int, py(1.0)]) -> None: - ^^^^^^^ -GuppyError: Compile-time `py(...)` expression with type `` is not a valid type argument +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/invalid_instantiate1.err b/tests/error/struct_errors/invalid_instantiate1.err index 0d78a3d4..aa2b1887 100644 --- a/tests/error/struct_errors/invalid_instantiate1.err +++ b/tests/error/struct_errors/invalid_instantiate1.err @@ -1,6 +1,9 @@ -Guppy compilation failed. Error in file $FILE:17 +Error: Missing type arguments (at $FILE:17:11) + | +15 | +16 | @guppy(module) +17 | def foo(s: MyStruct) -> None: + | ^^^^^^^^ Missing type argument for type `MyStruct` (expected 1, got + | 0) -15: @guppy(module) -16: def foo(s: MyStruct) -> None: - ^^^^^^^^ -GuppyError: Missing parameter for type `MyStruct` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/struct_errors/invalid_instantiate2.err b/tests/error/struct_errors/invalid_instantiate2.err index 61c04f56..ca0dd175 100644 --- a/tests/error/struct_errors/invalid_instantiate2.err +++ b/tests/error/struct_errors/invalid_instantiate2.err @@ -1,6 +1,9 @@ -Guppy compilation failed. Error in file $FILE:17 +Error: Too many type arguments (at $FILE:17:11) + | +15 | +16 | @guppy(module) +17 | def foo(s: MyStruct[int, bool]) -> None: + | ^^^^^^^^^^^^^^^^^^^ Unexpected type argument for type `MyStruct` (expected 1, + | got 2) -15: @guppy(module) -16: def foo(s: MyStruct[int, bool]) -> None: - ^^^^^^^^^^^^^^^^^^^ -GuppyError: Too many parameters for type `MyStruct` +Guppy compilation failed due to 1 previous error From 130282dfe28112418c3af8f3c68f80a33d37c979 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:04:09 +0000 Subject: [PATCH 17/20] feat: Update remaining code to use new diagnostics (#606) --- guppylang/cfg/builder.py | 38 ++++++++++---- guppylang/compiler/expr_compiler.py | 15 +++--- guppylang/decorator.py | 12 ++--- guppylang/definition/struct.py | 7 +-- guppylang/experimental.py | 33 ++++++++---- guppylang/module.py | 10 ++-- guppylang/prelude/_internal/checker.py | 52 +++++++++---------- .../array_errors/new_array_elem_mismatch1.err | 13 ++--- .../illegal_short_circuit.err | 13 ++--- .../comprehension_errors/illegal_ternary.err | 13 ++--- .../comprehension_errors/illegal_walrus.err | 13 ++--- .../experimental_errors/function_tensor.err | 17 +++--- .../list_comprehension.err | 17 +++--- .../experimental_errors/list_literal.err | 17 +++--- tests/error/experimental_errors/list_type.err | 16 ++++-- tests/error/misc_errors/no_return.err | 13 ++--- .../misc_errors/result_array_not_numeric.err | 16 +++--- .../error/misc_errors/result_value_linear.err | 15 +++--- tests/error/misc_errors/unreachable_1.err | 13 ++--- tests/error/misc_errors/unreachable_2.err | 13 ++--- tests/error/misc_errors/unreachable_3.err | 13 ++--- tests/error/misc_errors/unreachable_4.err | 13 ++--- tests/error/misc_errors/unreachable_5.err | 13 ++--- tests/error/py_errors/no_args.err | 13 ++--- tests/error/test_misc_errors.py | 2 +- 25 files changed, 240 insertions(+), 170 deletions(-) diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index cd76fb8c..d2ca6590 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -2,7 +2,8 @@ import copy import itertools from collections.abc import Iterator -from typing import NamedTuple +from dataclasses import dataclass +from typing import ClassVar, NamedTuple from guppylang.ast_util import ( AstVisitor, @@ -15,6 +16,8 @@ from guppylang.cfg.bb import BB, BBStatement from guppylang.cfg.cfg import CFG from guppylang.checker.core import Globals +from guppylang.checker.errors.generic import ExpectedError, UnsupportedError +from guppylang.diagnostic import Error from guppylang.error import GuppyError, InternalGuppyError from guppylang.experimental import check_lists_enabled from guppylang.nodes import ( @@ -47,6 +50,12 @@ class Jumps(NamedTuple): break_bb: BB | None +@dataclass(frozen=True) +class UnreachableError(Error): + title: ClassVar[str] = "Unreachable" + span_label: ClassVar[str] = "This code is not reachable" + + class CFGBuilder(AstVisitor[BB | None]): """Constructs a CFG from ast nodes.""" @@ -71,7 +80,7 @@ def build(self, nodes: list[ast.stmt], returns_none: bool, globals: Globals) -> # an implicit void return if final_bb is not None: if not returns_none: - raise GuppyError("Expected return statement", nodes[-1]) + raise GuppyError(ExpectedError(nodes[-1], "return statement")) self.cfg.link(final_bb, self.cfg.exit_bb) return self.cfg @@ -81,7 +90,7 @@ def visit_stmts(self, nodes: list[ast.stmt], bb: BB, jumps: Jumps) -> BB | None: next_functional = False for node in nodes: if bb_opt is None: - raise GuppyError("Unreachable code", node) + raise GuppyError(UnreachableError(node)) if is_functional_annotation(node): next_functional = True continue @@ -241,7 +250,7 @@ def visit_FunctionDef( def generic_visit(self, node: ast.AST, bb: BB, jumps: Jumps) -> BB | None: # When adding support for new statements, we have to remember to use the # ExprBuilder to transform all included expressions! - raise GuppyError("Statement is not supported", node) + raise GuppyError(UnsupportedError(node, "This statement", singular=True)) class ExprBuilder(ast.NodeTransformer): @@ -309,16 +318,20 @@ def visit_ListComp(self, node: ast.ListComp) -> ast.AST: # Check for illegal expressions illegals = find_nodes(is_illegal_in_list_comp, node) if illegals: - raise GuppyError( - "Expression is not supported inside a list comprehension", illegals[0] + err = UnsupportedError( + illegals[0], + "This expression", + singular=True, + unsupported_in="a list comprehension", ) + raise GuppyError(err) # Desugar into statements that create the iterator, check for a next element, # get the next element, and finalise the iterator. gens = [] for g in node.generators: if g.is_async: - raise GuppyError("Async generators are not supported", g) + raise GuppyError(UnsupportedError(g, "Async generators")) g.iter = self.visit(g.iter) it = make_var(next(tmp_vars), g.iter) hasnext = make_var(next(tmp_vars), g.iter) @@ -479,6 +492,12 @@ def is_functional_annotation(stmt: ast.stmt) -> bool: return False +@dataclass(frozen=True) +class EmptyPyExprError(Error): + title: ClassVar[str] = "Invalid Python expression" + span_label: ClassVar[str] = "Compile-time `py(...)` expression requires an argument" + + def is_py_expression(node: ast.AST) -> PyExpr | None: """Checks if the given node is a compile-time `py(...)` expression and turns it into a `PyExpr` AST node. @@ -492,10 +511,7 @@ def is_py_expression(node: ast.AST) -> PyExpr | None: ): match node.args: case []: - raise GuppyError( - "Compile-time `py(...)` expression requires an argument", - node, - ) + raise GuppyError(EmptyPyExprError(node)) case [arg]: pass case args: diff --git a/guppylang/compiler/expr_compiler.py b/guppylang/compiler/expr_compiler.py index fefe6736..7bfd16a3 100644 --- a/guppylang/compiler/expr_compiler.py +++ b/guppylang/compiler/expr_compiler.py @@ -18,6 +18,7 @@ from guppylang.ast_util import AstVisitor, get_type, with_loc, with_type from guppylang.cfg.builder import tmp_vars from guppylang.checker.core import Variable +from guppylang.checker.errors.generic import UnsupportedError from guppylang.checker.linearity_checker import contains_subscript from guppylang.compiler.core import CompilerBase, DFContainer from guppylang.compiler.hugr_extension import PartialOp @@ -188,11 +189,11 @@ def visit_GlobalName(self, node: GlobalName) -> Wire: defn = self.globals[node.def_id] assert isinstance(defn, CompiledValueDef) if isinstance(defn, CompiledCallableDef) and defn.ty.parametrized: - raise GuppyError( - "Usage of polymorphic functions as dynamic higher-order values is not " - "supported yet", - node, + # TODO: This should be caught during checking + err = UnsupportedError( + node, "Polymorphic functions as dynamic higher-order values" ) + raise GuppyError(err) return defn.load(self.dfg, self.globals, node) def visit_Name(self, node: ast.Name) -> Wire: @@ -379,10 +380,10 @@ def visit_TypeApply(self, node: TypeApply) -> Wire: # TODO: We would need to do manual monomorphisation in that case to obtain a # function that returns two ports as expected if instantiation_needs_unpacking(defn.ty, node.inst): - raise GuppyError( - "Generic function instantiations returning rows are not supported yet", - node, + err = UnsupportedError( + node, "Generic function instantiations returning rows" ) + raise GuppyError(err) return defn.load_with_args(node.inst, self.dfg, self.globals, node) diff --git a/guppylang/decorator.py b/guppylang/decorator.py index 09bd85ca..e7a41dd6 100644 --- a/guppylang/decorator.py +++ b/guppylang/decorator.py @@ -32,7 +32,7 @@ from guppylang.definition.parameter import ConstVarDef, TypeVarDef from guppylang.definition.struct import RawStructDef from guppylang.definition.ty import OpaqueTypeDef, TypeDef -from guppylang.error import GuppyError, MissingModuleError, pretty_errors +from guppylang.error import MissingModuleError, pretty_errors from guppylang.ipython_inspect import get_ipython_globals, is_running_ipython from guppylang.module import ( GuppyModule, @@ -148,7 +148,7 @@ def _get_python_caller(self, fn: PyFunc | None = None) -> ModuleIdentifier: break frame = frame.f_back else: - raise GuppyError("Could not find a caller for the `@guppy` decorator") + raise RuntimeError("Could not find a caller for the `@guppy` decorator") # Jupyter notebook cells all get different dummy filenames. However, # we want the whole notebook to correspond to a single implicit @@ -172,7 +172,7 @@ def init_module(self, import_builtins: bool = True) -> None: module_id = self._get_python_caller() if module_id in self._modules: msg = f"Module {module_id.name} is already initialised" - raise GuppyError(msg) + raise ValueError(msg) self._modules[module_id] = GuppyModule(module_id.name, import_builtins) @pretty_errors @@ -426,7 +426,7 @@ def get_module( other_module = find_guppy_module_in_py_module(value) if other_module and other_module != module: defs[x] = value - except GuppyError: + except ValueError: pass module.load(**defs) return module @@ -447,7 +447,7 @@ def compile_function(self, f_def: RawFunctionDef) -> FuncDefnPointer: """Compiles a single function definition.""" module = f_def.id.module if not module: - raise GuppyError("Function definition must belong to a module") + raise ValueError("Function definition must belong to a module") compiled_module = module.compile() assert module._compiled is not None, "Module should be compiled" globs = module._compiled.globs @@ -476,7 +476,7 @@ def _parse_expr_string(ty_str: str, parse_err: str, sources: SourceMap) -> ast.e try: expr_ast = ast.parse(ty_str, mode="eval").body except SyntaxError: - raise GuppyError(parse_err) from None + raise SyntaxError(parse_err) from None # Try to annotate the type AST with source information. This requires us to # inspect the stack frame of the caller diff --git a/guppylang/definition/struct.py b/guppylang/definition/struct.py index e20a8cbb..39507bad 100644 --- a/guppylang/definition/struct.py +++ b/guppylang/definition/struct.py @@ -80,6 +80,9 @@ class Suggestion(Help): "Add a `@guppy` annotation to turn `{method_name}` into a Guppy method" ) + def __post_init__(self) -> None: + self.add_sub_diagnostic(NonGuppyMethodError.Suggestion(None)) + @dataclass(frozen=True) class RawStructDef(TypeDef, ParsableDef): @@ -132,9 +135,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedStructDef": case _, ast.FunctionDef(name=name) as node: v = getattr(self.python_class, name) if not isinstance(v, Definition): - err = NonGuppyMethodError(node, self.name, name) - err.add_sub_diagnostic(NonGuppyMethodError.Suggestion(None)) - raise GuppyError(err) + raise GuppyError(NonGuppyMethodError(node, self.name, name)) used_func_names[name] = node if name in used_field_names: raise GuppyError(DuplicateFieldError(node, self.name, name)) diff --git a/guppylang/experimental.py b/guppylang/experimental.py index 369a27fd..b630be1a 100644 --- a/guppylang/experimental.py +++ b/guppylang/experimental.py @@ -1,7 +1,10 @@ from ast import expr +from dataclasses import dataclass from types import TracebackType +from typing import ClassVar from guppylang.ast_util import AstNode +from guppylang.diagnostic import Error, Help from guppylang.error import GuppyError EXPERIMENTAL_FEATURES_ENABLED = False @@ -55,19 +58,29 @@ def __exit__( EXPERIMENTAL_FEATURES_ENABLED = self.original +@dataclass(frozen=True) +class ExperimentalFeatureError(Error): + title: ClassVar[str] = "Experimental feature" + span_label: ClassVar[str] = "{things} are an experimental feature" + things: str + + @dataclass(frozen=True) + class Suggestion(Help): + message: ClassVar[str] = ( + "Experimental features are currently disabled. You can enable them by " + "calling `guppylang.enable_experimental_features()`, however note that " + "these features are unstable and might break in the future." + ) + + def __post_init__(self) -> None: + self.add_sub_diagnostic(ExperimentalFeatureError.Suggestion(None)) + + def check_function_tensors_enabled(node: expr | None = None) -> None: if not EXPERIMENTAL_FEATURES_ENABLED: - raise GuppyError( - "Function tensors are an experimental feature. Use " - "`guppylang.enable_experimental_features()` to enable them.", - node, - ) + raise GuppyError(ExperimentalFeatureError(node, "Function tensors")) def check_lists_enabled(loc: AstNode | None = None) -> None: if not EXPERIMENTAL_FEATURES_ENABLED: - raise GuppyError( - "Lists are an experimental feature and not fully supported yet. Use " - "`guppylang.enable_experimental_features()` to enable them.", - loc, - ) + raise GuppyError(ExperimentalFeatureError(loc, "Lists")) diff --git a/guppylang/module.py b/guppylang/module.py index 1efa3130..65b966ea 100644 --- a/guppylang/module.py +++ b/guppylang/module.py @@ -29,7 +29,7 @@ from guppylang.definition.parameter import ParamDef from guppylang.definition.struct import CheckedStructDef from guppylang.definition.ty import TypeDef -from guppylang.error import GuppyError, pretty_errors +from guppylang.error import pretty_errors from guppylang.experimental import enable_experimental_features if TYPE_CHECKING: @@ -143,7 +143,7 @@ def load( imports.append((alias, mod)) else: msg = f"Only Guppy definitions or modules can be imported. Got `{imp}`" - raise GuppyError(msg) + raise TypeError(msg) # Also include any impls that are defined by the imported modules impls: dict[DefId, dict[str, DefId]] = {} @@ -180,7 +180,7 @@ def load_all(self, mod: GuppyModule | ModuleType) -> None: self.load_all(find_guppy_module_in_py_module(mod)) else: msg = f"Only Guppy definitions or modules can be imported. Got `{mod}`" - raise GuppyError(msg) + raise TypeError(msg) def register_def(self, defn: RawDef, instance: TypeDef | None = None) -> None: """Registers a definition with this module. @@ -416,13 +416,13 @@ def find_guppy_module_in_py_module(module: ModuleType) -> GuppyModule: if not mods: msg = f"No Guppy modules found in `{module.__name__}`" - raise GuppyError(msg) + raise ValueError(msg) if len(mods) > 1: msg = ( f"Python module `{module.__name__}` contains multiple Guppy modules. " "Cannot decide which one to import." ) - raise GuppyError(msg) + raise ValueError(msg) return mods[0] diff --git a/guppylang/prelude/_internal/checker.py b/guppylang/prelude/_internal/checker.py index a7bc400a..c41212e2 100644 --- a/guppylang/prelude/_internal/checker.py +++ b/guppylang/prelude/_internal/checker.py @@ -1,7 +1,10 @@ import ast +from dataclasses import dataclass +from typing import ClassVar from guppylang.ast_util import AstNode, with_loc from guppylang.checker.core import Context +from guppylang.checker.errors.generic import UnsupportedError from guppylang.checker.expr_checker import ( ExprChecker, ExprSynthesizer, @@ -16,6 +19,7 @@ DefaultCallChecker, ) from guppylang.definition.value import CallableDef +from guppylang.diagnostic import Error, Note from guppylang.error import GuppyError, GuppyTypeError, InternalGuppyError from guppylang.nodes import GlobalCall, ResultExpr from guppylang.tys.arg import ConstArg, TypeArg @@ -75,22 +79,6 @@ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: return expr, ty -class FailingChecker(CustomCallChecker): - """Call checker for Python functions that are not available in Guppy. - - Gives the uses a nicer error message when they try to use an unsupported feature. - """ - - def __init__(self, msg: str) -> None: - self.msg = msg - - def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: - raise GuppyError(self.msg, self.node) - - def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: - raise GuppyError(self.msg, self.node) - - class UnsupportedChecker(CustomCallChecker): """Call checker for Python builtin functions that are not available in Guppy. @@ -98,14 +86,16 @@ class UnsupportedChecker(CustomCallChecker): """ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: - raise GuppyError( - f"Builtin method `{self.func.name}` is not supported by Guppy", self.node + err = UnsupportedError( + self.node, f"Builtin method `{self.func.name}`", singular=True ) + raise GuppyError(err) def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: - raise GuppyError( - f"Builtin method `{self.func.name}` is not supported by Guppy", self.node + err = UnsupportedError( + self.node, f"Builtin method `{self.func.name}`", singular=True ) + raise GuppyError(err) class DunderChecker(CustomCallChecker): @@ -236,6 +226,18 @@ def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: class ResultChecker(CustomCallChecker): """Call checker for the `result` function.""" + @dataclass(frozen=True) + class InvalidError(Error): + title: ClassVar[str] = "Invalid Result" + span_label: ClassVar[str] = "Expression of type `{ty}` is not a valid result." + ty: Type + + @dataclass(frozen=True) + class Explanation(Note): + message: ClassVar[str] = ( + "Only numeric values or arrays thereof are allowed as results" + ) + def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: check_num_args(2, len(args), self.node) [tag, value] = args @@ -243,10 +245,8 @@ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: raise GuppyTypeError("Expected a string literal", tag) value, ty = ExprSynthesizer(self.ctx).synthesize(value) # We only allow numeric values or vectors of numeric values - err = ( - f"Expression of type `{ty}` is not a valid result. Only numeric values or " - "arrays thereof are allowed." - ) + err = ResultChecker.InvalidError(value, ty) + err.add_sub_diagnostic(ResultChecker.InvalidError.Explanation(None)) if self._is_numeric_or_bool_type(ty): base_ty = ty array_len: Const | None = None @@ -255,11 +255,11 @@ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: assert isinstance(ty_arg, TypeArg) assert isinstance(len_arg, ConstArg) if not self._is_numeric_or_bool_type(ty_arg.ty): - raise GuppyError(err, value) + raise GuppyError(err) base_ty = ty_arg.ty array_len = len_arg.const else: - raise GuppyError(err, value) + raise GuppyError(err) node = ResultExpr(value, base_ty, array_len, tag.value) return with_loc(self.node, node), NoneType() diff --git a/tests/error/array_errors/new_array_elem_mismatch1.err b/tests/error/array_errors/new_array_elem_mismatch1.err index 8d587c7e..5ee9e0d9 100644 --- a/tests/error/array_errors/new_array_elem_mismatch1.err +++ b/tests/error/array_errors/new_array_elem_mismatch1.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Expected return statement (at $FILE:13:4) + | +11 | @guppy(module) +12 | def main() -> array[int, 1]: +13 | array(False) + | ^^^^^^^^^^^^ Expected return statement -11: @guppy(module) -12: def main() -> array[int, 1]: -13: array(False) - ^^^^^^^^^^^^ -GuppyError: Expected return statement +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/illegal_short_circuit.err b/tests/error/comprehension_errors/illegal_short_circuit.err index d05dc6a8..08e58f8e 100644 --- a/tests/error/comprehension_errors/illegal_short_circuit.err +++ b/tests/error/comprehension_errors/illegal_short_circuit.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Unsupported (at $FILE:6:22) + | +4 | @compile_guppy +5 | def foo(xs: list[int]) -> None: +6 | [x for x in xs if x < 5 and x != 6] + | ^^^^^^^^^^^^^^^^ This expression is not supported in a list comprehension -4: @compile_guppy -5: def foo(xs: list[int]) -> None: -6: [x for x in xs if x < 5 and x != 6] - ^^^^^^^^^^^^^^^^ -GuppyError: Expression is not supported inside a list comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/illegal_ternary.err b/tests/error/comprehension_errors/illegal_ternary.err index d99720ac..a5efa9e9 100644 --- a/tests/error/comprehension_errors/illegal_ternary.err +++ b/tests/error/comprehension_errors/illegal_ternary.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Unsupported (at $FILE:6:17) + | +4 | @compile_guppy +5 | def foo(xs: list[int], ys: list[int], b: bool) -> None: +6 | [x for x in (xs if b else ys)] + | ^^^^^^^^^^^^^^^ This expression is not supported in a list comprehension -4: @compile_guppy -5: def foo(xs: list[int], ys: list[int], b: bool) -> None: -6: [x for x in (xs if b else ys)] - ^^^^^^^^^^^^^^^ -GuppyError: Expression is not supported inside a list comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/comprehension_errors/illegal_walrus.err b/tests/error/comprehension_errors/illegal_walrus.err index 4b19d4ba..839c48b9 100644 --- a/tests/error/comprehension_errors/illegal_walrus.err +++ b/tests/error/comprehension_errors/illegal_walrus.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Unsupported (at $FILE:6:5) + | +4 | @compile_guppy +5 | def foo(xs: list[int]) -> None: +6 | [y := x for x in xs] + | ^^^^^^ This expression is not supported in a list comprehension -4: @compile_guppy -5: def foo(xs: list[int]) -> None: -6: [y := x for x in xs] - ^^^^^^ -GuppyError: Expression is not supported inside a list comprehension +Guppy compilation failed due to 1 previous error diff --git a/tests/error/experimental_errors/function_tensor.err b/tests/error/experimental_errors/function_tensor.err index 4974e110..b3163311 100644 --- a/tests/error/experimental_errors/function_tensor.err +++ b/tests/error/experimental_errors/function_tensor.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:19 +Error: Experimental feature (at $FILE:19:11) + | +17 | @guppy(module) +18 | def main() -> tuple[int, int]: +19 | return (f, g)(1, 2) + | ^^^^^^ Function tensors are an experimental feature -17: @guppy(module) -18: def main() -> tuple[int, int]: -19: return (f, g)(1, 2) - ^^^^^^ -GuppyError: Function tensors are an experimental feature. Use `guppylang.enable_experimental_features()` to enable them. +Help: Experimental features are currently disabled. You can enable them by +calling `guppylang.enable_experimental_features()`, however note that these +features are unstable and might break in the future. + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/experimental_errors/list_comprehension.err b/tests/error/experimental_errors/list_comprehension.err index e2be624e..6b6b91fe 100644 --- a/tests/error/experimental_errors/list_comprehension.err +++ b/tests/error/experimental_errors/list_comprehension.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Experimental feature (at $FILE:9:4) + | +7 | @guppy(module) +8 | def main() -> None: +9 | [i for i in range(10)] + | ^^^^^^^^^^^^^^^^^^^^^^ Lists are an experimental feature -7: @guppy(module) -8: def main() -> None: -9: [i for i in range(10)] - ^^^^^^^^^^^^^^^^^^^^^^ -GuppyError: Lists are an experimental feature and not fully supported yet. Use `guppylang.enable_experimental_features()` to enable them. +Help: Experimental features are currently disabled. You can enable them by +calling `guppylang.enable_experimental_features()`, however note that these +features are unstable and might break in the future. + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/experimental_errors/list_literal.err b/tests/error/experimental_errors/list_literal.err index 717c886b..94a65341 100644 --- a/tests/error/experimental_errors/list_literal.err +++ b/tests/error/experimental_errors/list_literal.err @@ -1,7 +1,12 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Experimental feature (at $FILE:9:4) + | +7 | @guppy(module) +8 | def main() -> None: +9 | [1, 2, 3] + | ^^^^^^^^^ Lists are an experimental feature -7: @guppy(module) -8: def main() -> None: -9: [1, 2, 3] - ^^^^^^^^^ -GuppyError: Lists are an experimental feature and not fully supported yet. Use `guppylang.enable_experimental_features()` to enable them. +Help: Experimental features are currently disabled. You can enable them by +calling `guppylang.enable_experimental_features()`, however note that these +features are unstable and might break in the future. + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/experimental_errors/list_type.err b/tests/error/experimental_errors/list_type.err index cf5446ab..da351726 100644 --- a/tests/error/experimental_errors/list_type.err +++ b/tests/error/experimental_errors/list_type.err @@ -1,6 +1,12 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Experimental feature (at $FILE:8:12) + | +6 | +7 | @guppy(module) +8 | def main(x: list[int]) -> list[int]: + | ^^^^^^^^^ Lists are an experimental feature -6: @guppy(module) -7: def main(x: list[int]) -> list[int]: - ^^^^^^^^^ -GuppyError: Lists are an experimental feature and not fully supported yet. Use `guppylang.enable_experimental_features()` to enable them. +Help: Experimental features are currently disabled. You can enable them by +calling `guppylang.enable_experimental_features()`, however note that these +features are unstable and might break in the future. + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/no_return.err b/tests/error/misc_errors/no_return.err index 676e2bf1..e4414a55 100644 --- a/tests/error/misc_errors/no_return.err +++ b/tests/error/misc_errors/no_return.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Expected return statement (at $FILE:6:4) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | pass + | ^^^^ Expected return statement -4: @compile_guppy -5: def foo() -> int: -6: pass - ^^^^ -GuppyError: Expected return statement +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/result_array_not_numeric.err b/tests/error/misc_errors/result_array_not_numeric.err index f7575eba..3069b478 100644 --- a/tests/error/misc_errors/result_array_not_numeric.err +++ b/tests/error/misc_errors/result_array_not_numeric.err @@ -1,7 +1,11 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Invalid Result (at $FILE:7:18) + | +5 | @compile_guppy +6 | def foo(x: array[tuple[int, bool], 42]) -> None: +7 | result("foo", x) + | ^ Expression of type `array[(int, bool), 42]` is not a valid + | result. -5: @compile_guppy -6: def foo(x: array[tuple[int, bool], 42]) -> None: -7: result("foo", x) - ^ -GuppyError: Expression of type `array[(int, bool), 42]` is not a valid result. Only numeric values or arrays thereof are allowed. +Note: Only numeric values or arrays thereof are allowed as results + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/result_value_linear.err b/tests/error/misc_errors/result_value_linear.err index 514cc748..8f0bcb88 100644 --- a/tests/error/misc_errors/result_value_linear.err +++ b/tests/error/misc_errors/result_value_linear.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:14 +Error: Invalid Result (at $FILE:14:18) + | +12 | @guppy(module) +13 | def foo(q: qubit) -> None: +14 | result("foo", q) + | ^ Expression of type `qubit` is not a valid result. -12: @guppy(module) -13: def foo(q: qubit) -> None: -14: result("foo", q) - ^ -GuppyError: Expression of type `qubit` is not a valid result. Only numeric values or arrays thereof are allowed. +Note: Only numeric values or arrays thereof are allowed as results + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_1.err b/tests/error/misc_errors/unreachable_1.err index 2b007477..c2beadd7 100644 --- a/tests/error/misc_errors/unreachable_1.err +++ b/tests/error/misc_errors/unreachable_1.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Unreachable (at $FILE:7:4) + | +5 | def foo() -> int: +6 | return 0 +7 | x = 42 + | ^^^^^^ This code is not reachable -5: def foo() -> int: -6: return 0 -7: x = 42 - ^^^^^^ -GuppyError: Unreachable code +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_2.err b/tests/error/misc_errors/unreachable_2.err index 8ee3f03f..77ba347f 100644 --- a/tests/error/misc_errors/unreachable_2.err +++ b/tests/error/misc_errors/unreachable_2.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:10 +Error: Unreachable (at $FILE:10:4) + | + 8 | else: + 9 | return 1 +10 | x = 42 + | ^^^^^^ This code is not reachable -8: else: -9: return 1 -10: x = 42 - ^^^^^^ -GuppyError: Unreachable code +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_3.err b/tests/error/misc_errors/unreachable_3.err index 007f5686..ea97ab28 100644 --- a/tests/error/misc_errors/unreachable_3.err +++ b/tests/error/misc_errors/unreachable_3.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Unreachable (at $FILE:8:8) + | +6 | while x: +7 | break +8 | x = 42 + | ^^^^^^ This code is not reachable -6: while x: -7: break -8: x = 42 - ^^^^^^ -GuppyError: Unreachable code +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_4.err b/tests/error/misc_errors/unreachable_4.err index feeb2be2..7c96a925 100644 --- a/tests/error/misc_errors/unreachable_4.err +++ b/tests/error/misc_errors/unreachable_4.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Unreachable (at $FILE:8:8) + | +6 | while x: +7 | continue +8 | x = 42 + | ^^^^^^ This code is not reachable -6: while x: -7: continue -8: x = 42 - ^^^^^^ -GuppyError: Unreachable code +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/unreachable_5.err b/tests/error/misc_errors/unreachable_5.err index 44b49c39..3a34b1e4 100644 --- a/tests/error/misc_errors/unreachable_5.err +++ b/tests/error/misc_errors/unreachable_5.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:8 +Error: Unreachable (at $FILE:8:8) + | +6 | while x: +7 | return 42 +8 | x = 42 + | ^^^^^^ This code is not reachable -6: while x: -7: return 42 -8: x = 42 - ^^^^^^ -GuppyError: Unreachable code +Guppy compilation failed due to 1 previous error diff --git a/tests/error/py_errors/no_args.err b/tests/error/py_errors/no_args.err index 7c30ee04..c2af2608 100644 --- a/tests/error/py_errors/no_args.err +++ b/tests/error/py_errors/no_args.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Invalid Python expression (at $FILE:6:11) + | +4 | @compile_guppy +5 | def foo() -> int: +6 | return py() + | ^^^^ Compile-time `py(...)` expression requires an argument -4: @compile_guppy -5: def foo() -> int: -6: return py() - ^^^^ -GuppyError: Compile-time `py(...)` expression requires an argument +Guppy compilation failed due to 1 previous error diff --git a/tests/error/test_misc_errors.py b/tests/error/test_misc_errors.py index 49bf970c..b7ed0fab 100644 --- a/tests/error/test_misc_errors.py +++ b/tests/error/test_misc_errors.py @@ -27,5 +27,5 @@ def test_misc_errors(file, capsys, snapshot): def test_extern_bad_type_syntax(): module = GuppyModule("test") - with pytest.raises(GuppyError, match="Not a valid Guppy type: `foo bar`"): + with pytest.raises(SyntaxError, match="Not a valid Guppy type: `foo bar`"): guppy.extern(name="x", ty="foo bar", module=module) From 4a61fada2ebe5b720d5363c0fb072d168e9e96e0 Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Mon, 11 Nov 2024 15:18:13 +0000 Subject: [PATCH 18/20] Fix ruff --- guppylang/prelude/_internal/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guppylang/prelude/_internal/checker.py b/guppylang/prelude/_internal/checker.py index c3119389..083e50c1 100644 --- a/guppylang/prelude/_internal/checker.py +++ b/guppylang/prelude/_internal/checker.py @@ -1,6 +1,6 @@ import ast from dataclasses import dataclass -from typing import cast, ClassVar +from typing import ClassVar, cast from guppylang.ast_util import AstNode, with_loc, with_type from guppylang.checker.core import Context From 2ef864ec486b4cc855337cdaf8a742c7e21e00e8 Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:25:20 +0000 Subject: [PATCH 19/20] feat: Get rid of old error system (#635) Closes #633 Also updates a few last errors that I overlooked in the previous refactors --- guppylang/checker/errors/type_errors.py | 27 ++++ guppylang/checker/expr_checker.py | 13 +- guppylang/checker/stmt_checker.py | 7 +- guppylang/error.py | 118 ++---------------- guppylang/prelude/_internal/checker.py | 88 ++++++------- guppylang/tys/param.py | 21 ++-- .../array_errors/new_array_cannot_infer.err | 15 ++- .../array_errors/new_array_check_fail.err | 13 +- .../array_errors/new_array_wrong_length.err | 14 ++- .../misc_errors/result_tag_not_static.err | 13 +- .../error/misc_errors/result_tag_not_str.err | 13 +- .../error/misc_errors/result_tag_too_big.err | 15 ++- tests/error/poly_errors/arg_mismatch4.err | 12 +- tests/error/poly_errors/arg_mismatch5.err | 12 +- tests/error/poly_errors/non_linear1.err | 14 ++- tests/error/poly_errors/non_linear2.err | 15 ++- tests/error/poly_errors/non_linear3.err | 15 ++- tests/error/type_errors/call_not_function.err | 13 +- 18 files changed, 198 insertions(+), 240 deletions(-) diff --git a/guppylang/checker/errors/type_errors.py b/guppylang/checker/errors/type_errors.py index 1b069ed6..c87bbdc9 100644 --- a/guppylang/checker/errors/type_errors.py +++ b/guppylang/checker/errors/type_errors.py @@ -8,6 +8,7 @@ if TYPE_CHECKING: from guppylang.definition.struct import StructField from guppylang.tys.const import Const + from guppylang.tys.param import Parameter from guppylang.tys.ty import FunctionType, Type @@ -49,6 +50,18 @@ class AssignFieldTypeMismatchError(Error): field: StructField +@dataclass(frozen=True) +class NonLinearInstantiateError(Error): + title: ClassVar[str] = "Not defined for linear argument" + span_label: ClassVar[str] = ( + "Cannot instantiate non-linear type parameter `{param.name}` in type " + "`{func_ty}` with linear type `{ty}`" + ) + param: Parameter + func_ty: FunctionType + ty: Type + + @dataclass(frozen=True) class TypeInferenceError(Error): title: ClassVar[str] = "Cannot infer type" @@ -125,6 +138,20 @@ class BadSignature(Help): act_signature: FunctionType +@dataclass(frozen=True) +class MissingReturnValueError(Error): + title: ClassVar[str] = "Missing return value" + span_label: ClassVar[str] = "Expected return value of type `{ty}`" + ty: Type + + +@dataclass(frozen=True) +class NotCallableError(Error): + title: ClassVar[str] = "Not callable" + span_label: ClassVar[str] = "Expected a function, got expression of type `{actual}`" + actual: Type + + @dataclass(frozen=True) class WrongNumberOfArgsError(Error): title: ClassVar[str] = "" # Custom implementation in `rendered_title` diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index c4243548..db5449a3 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -48,6 +48,7 @@ UnsupportedError, Variable, ) +from guppylang.checker.errors.generic import ExpectedError from guppylang.checker.errors.linearity import LinearForBreakError from guppylang.checker.errors.py_errors import ( IllegalPyExpressionError, @@ -63,6 +64,8 @@ BinaryOperatorNotDefinedError, IllegalConstant, ModuleMemberNotFoundError, + NonLinearInstantiateError, + NotCallableError, TypeInferenceError, TypeMismatchError, UnaryOperatorNotDefinedError, @@ -303,7 +306,7 @@ def visit_Call(self, node: ast.Call, ty: Type) -> tuple[ast.expr, Subst]: elif callee := self.ctx.globals.get_instance_func(func_ty, "__call__"): return callee.check_call(node.args, ty, node, self.ctx) else: - raise GuppyTypeError(f"Expected function type, got `{func_ty}`", node.func) + raise GuppyTypeError(NotCallableError(node.func, func_ty)) def visit_PyExpr(self, node: PyExpr, ty: Type) -> tuple[ast.expr, Subst]: python_val = eval_py_expr(node, self.ctx) @@ -388,7 +391,7 @@ def _check_global( return with_loc(node, GlobalName(id=name, def_id=constr.id)), constr.ty case defn: raise GuppyError( - f"Expected a value, got {defn.description} `{name}`", node + ExpectedError(node, "a value", got=f"{defn.description} `{name}`") ) def visit_Attribute(self, node: ast.Attribute) -> tuple[ast.expr, Type]: @@ -635,7 +638,7 @@ def visit_Call(self, node: ast.Call) -> tuple[ast.expr, Type]: elif f := self.ctx.globals.get_instance_func(ty, "__call__"): return f.synthesize_call(node.args, node, self.ctx) else: - raise GuppyTypeError(f"Expected function type, got `{ty}`", node.func) + raise GuppyTypeError(NotCallableError(node.func, ty)) def visit_MakeIter(self, node: MakeIter) -> tuple[ast.expr, Type]: node.value, ty = self.synthesize(node.value) @@ -985,9 +988,7 @@ def check_inst(func_ty: FunctionType, inst: Inst, node: AstNode) -> None: and not param.can_be_linear ): raise GuppyTypeError( - f"Cannot instantiate non-linear type variable `{param.name}` in type " - f"`{func_ty}` with linear type `{arg.ty}`", - node, + NonLinearInstantiateError(node, param, func_ty, arg.ty) ) # For everything else, we fall back to the default checking implementation param.check_arg(arg, node) diff --git a/guppylang/checker/stmt_checker.py b/guppylang/checker/stmt_checker.py index ba795fff..3f037f51 100644 --- a/guppylang/checker/stmt_checker.py +++ b/guppylang/checker/stmt_checker.py @@ -18,6 +18,7 @@ AssignFieldTypeMismatchError, AssignNonPlaceHelp, AttributeNotFoundError, + MissingReturnValueError, WrongNumberOfUnpacksError, ) from guppylang.checker.expr_checker import ExprChecker, ExprSynthesizer @@ -132,7 +133,7 @@ def _check_assign(self, lhs: ast.expr, ty: Type, node: ast.stmt) -> ast.expr: def visit_Assign(self, node: ast.Assign) -> ast.Assign: if len(node.targets) > 1: # This is the case for assignments like `a = b = 1` - raise GuppyError("Multi assignment not supported", node) + raise GuppyError(UnsupportedError(node, "Multi assignments")) [target] = node.targets node.value, ty = self._synth_expr(node.value) @@ -171,9 +172,7 @@ def visit_Return(self, node: ast.Return) -> ast.stmt: ) assert len(subst) == 0 # `self.return_ty` is closed! elif not isinstance(self.return_ty, NoneType): - raise GuppyTypeError( - f"Expected return value of type `{self.return_ty}`", None - ) + raise GuppyTypeError(MissingReturnValueError(node, self.return_ty)) return node def visit_NestedFunctionDef(self, node: NestedFunctionDef) -> ast.stmt: diff --git a/guppylang/error.py b/guppylang/error.py index 72c47683..d4f24edf 100644 --- a/guppylang/error.py +++ b/guppylang/error.py @@ -1,74 +1,23 @@ -import ast import functools import os import sys -import textwrap -from collections.abc import Callable, Iterator, Sequence +from collections.abc import Callable, Iterator from contextlib import contextmanager -from dataclasses import dataclass, field +from dataclasses import dataclass from types import TracebackType 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: - """A source location associated with an AST node. - - This class translates the location data provided by the ast module into a location - inside the file. - """ - - file: str - line: int - col: int - ast_node: AstNode | None - - @staticmethod - def from_ast(node: AstNode) -> "SourceLoc": - file, line_offset = get_file(node), get_line_offset(node) - assert file is not None - assert line_offset is not None - return SourceLoc(file, line_offset + node.lineno - 1, node.col_offset, node) - - def __str__(self) -> str: - return f"{self.line}:{self.col}" - - def __lt__(self, other: Any) -> bool: - if not isinstance(other, SourceLoc): - return NotImplemented - return (self.line, self.col) < (other.line, other.col) + from guppylang.diagnostic import Error, Fatal @dataclass class GuppyError(Exception): - """General Guppy error tied to a node in the AST. + """An error that occurs during compilation.""" - 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 | 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) - - def get_msg(self) -> str: - """Returns the message associated with this error. - - 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 "???" - for loc in self.locs_in_msg - ) - ) + error: "Error | Fatal" class GuppyTypeError(GuppyError): @@ -79,7 +28,7 @@ class GuppyTypeInferenceError(GuppyError): """Special Guppy exception for type inference errors.""" -class MissingModuleError(GuppyError): +class MissingModuleError(Exception): """Special Guppy exception for operations that require a guppy module.""" @@ -122,35 +71,6 @@ def ipython_excepthook( sys.excepthook = old_hook -def format_source_location( - loc: ast.AST, - num_lines: int = 3, - indent: int = 4, -) -> str: - """Creates a pretty banner to show source locations for errors.""" - source, line_offset = get_source(loc), get_line_offset(loc) - assert source is not None - assert line_offset is not None - source_lines = source.splitlines(keepends=True) - end_col_offset = loc.end_col_offset - if end_col_offset is None or (loc.end_lineno and loc.end_lineno > loc.lineno): - end_col_offset = len(source_lines[loc.lineno - 1]) - 1 - s = "".join(source_lines[max(loc.lineno - num_lines, 0) : loc.lineno]).rstrip() - s += "\n" + loc.col_offset * " " + (end_col_offset - loc.col_offset) * "^" - s = textwrap.dedent(s).splitlines() - # Add line numbers - line_numbers = [ - str(line_offset + loc.lineno - i) + ":" for i in range(num_lines, 0, -1) - ] - longest = max(len(ln) for ln in line_numbers) - prefixes = [ln + " " * (longest - len(ln) + indent) for ln in line_numbers] - res = "".join( - prefix + line + "\n" for prefix, line in zip(prefixes, s[:-1], strict=False) - ) - res += (longest + indent) * " " + s[-1] - return res - - FuncT = TypeVar("FuncT", bound=Callable[..., Any]) @@ -161,34 +81,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): + if isinstance(err, GuppyError): from guppylang.decorator import guppy + from guppylang.diagnostic import DiagnosticsRenderer renderer = DiagnosticsRenderer(guppy._sources) - renderer.render_diagnostic(err.raw_msg) + renderer.render_diagnostic(err.error) 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: - sys.__excepthook__(excty, err, traceback) - return - - loc = err.location - file, line_offset = get_file(loc), get_line_offset(loc) - assert file is not None - assert line_offset is not None - line = line_offset + loc.lineno - 1 - sys.stderr.write( - f"Guppy compilation failed. Error in file {file}:{line}\n\n" - f"{format_source_location(loc)}\n" - f"{err.__class__.__name__}: {err.get_msg()}\n", - ) + # If it's not a GuppyError, fall back to default hook + sys.__excepthook__(excty, err, traceback) @functools.wraps(f) def pretty_errors_wrapped(*args: Any, **kwargs: Any) -> Any: diff --git a/guppylang/prelude/_internal/checker.py b/guppylang/prelude/_internal/checker.py index 083e50c1..b1bd4cbe 100644 --- a/guppylang/prelude/_internal/checker.py +++ b/guppylang/prelude/_internal/checker.py @@ -4,7 +4,8 @@ from guppylang.ast_util import AstNode, with_loc, with_type from guppylang.checker.core import Context -from guppylang.checker.errors.generic import UnsupportedError +from guppylang.checker.errors.generic import ExpectedError, UnsupportedError +from guppylang.checker.errors.type_errors import TypeMismatchError from guppylang.checker.expr_checker import ( ExprChecker, ExprSynthesizer, @@ -19,13 +20,13 @@ DefaultCallChecker, ) from guppylang.definition.struct import CheckedStructDef, RawStructDef -from guppylang.definition.value import CallableDef from guppylang.diagnostic import Error, Note from guppylang.error import GuppyError, GuppyTypeError, InternalGuppyError from guppylang.nodes import GlobalCall, ResultExpr from guppylang.tys.arg import ConstArg, TypeArg from guppylang.tys.builtin import ( array_type, + array_type_def, bool_type, int_type, is_array_type, @@ -40,7 +41,6 @@ NumericType, StructType, Type, - unify, ) @@ -112,26 +112,16 @@ def __init__(self, dunder_name: str, num_args: int = 1): self.dunder_name = dunder_name self.num_args = num_args - def _get_func(self, args: list[ast.expr]) -> tuple[list[ast.expr], CallableDef]: + def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: check_num_args(self.num_args, len(args), self.node) fst, *rest = args - fst, ty = ExprSynthesizer(self.ctx).synthesize(fst) - func = self.ctx.globals.get_instance_func(ty, self.dunder_name) - if func is None: - raise GuppyTypeError( - f"Builtin function `{self.func.name}` is not defined for argument of " - f"type `{ty}`", - self.node.args[0] if isinstance(self.node, ast.Call) else self.node, - ) - return [fst, *rest], func - - def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: - args, func = self._get_func(args) - return func.synthesize_call(args, self.node, self.ctx) - - def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: - args, func = self._get_func(args) - return func.check_call(args, ty, self.node, self.ctx) + return ExprSynthesizer(self.ctx).synthesize_instance_func( + fst, + rest, + self.dunder_name, + f"a valid argument to `{self.func.name}`", + give_reason=True, + ) class CallableChecker(CustomCallChecker): @@ -148,15 +138,6 @@ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: const = with_loc(self.node, ast.Constant(value=is_callable)) return const, bool_type() - def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: - args, _ = self.synthesize(args) - subst = unify(ty, bool_type(), {}) - if subst is None: - raise GuppyTypeError( - f"Expected expression of type `{ty}`, got `bool`", self.node - ) - return args, subst - class ArrayLenChecker(CustomCallChecker): """Function call checker for the `array.__len__` function.""" @@ -183,13 +164,22 @@ def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: class NewArrayChecker(CustomCallChecker): """Function call checker for the `array.__new__` function.""" + @dataclass(frozen=True) + class InferenceError(Error): + title: ClassVar[str] = "Cannot infer type" + span_label: ClassVar[str] = "Cannot infer the type of this array" + + @dataclass(frozen=True) + class Suggestion(Note): + message: ClassVar[str] = ( + "Consider adding a type annotation: `x: array[???] = ...`" + ) + def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: if len(args) == 0: - raise GuppyTypeError( - "Cannot infer the array element type. Consider adding a type " - "annotation.", - self.node, - ) + err = NewArrayChecker.InferenceError(self.node) + err.add_sub_diagnostic(NewArrayChecker.InferenceError.Suggestion(None)) + raise GuppyTypeError(err) [fst, *rest] = args fst, ty = ExprSynthesizer(self.ctx).synthesize(fst) checker = ExprChecker(self.ctx) @@ -204,9 +194,12 @@ def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: if not is_array_type(ty): - raise GuppyTypeError( - f"Expected expression of type `{ty}`, got `array`", self.node + dummy_array_ty = array_type_def.check_instantiate( + [p.to_existential()[0] for p in array_type_def.params], + self.ctx.globals, + self.node, ) + raise GuppyTypeError(TypeMismatchError(self.node, ty, dummy_array_ty)) match ty.args: case [TypeArg(ty=elem_ty), ConstArg(ConstValue(value=int(length)))]: subst: Subst = {} @@ -216,9 +209,7 @@ def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]: subst |= s if len(args) != length: raise GuppyTypeError( - f"Expected expression of type `{ty}`, got " - f"`array[{elem_ty}, {len(args)}]`", - self.node, + TypeMismatchError(self.node, ty, array_type(elem_ty, len(args))) ) call = GlobalCall(def_id=self.func.id, args=args, type_args=ty.args) return with_loc(self.node, call), subst @@ -245,15 +236,24 @@ class Explanation(Note): "Only numeric values or arrays thereof are allowed as results" ) + @dataclass(frozen=True) + class TooLongError(Error): + title: ClassVar[str] = "Tag too long" + span_label: ClassVar[str] = "Result tag is too long" + + @dataclass(frozen=True) + class Hint(Note): + message: ClassVar[str] = f"Result tags are limited to {TAG_MAX_LEN} bytes" + def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]: check_num_args(2, len(args), self.node) [tag, value] = args if not isinstance(tag, ast.Constant) or not isinstance(tag.value, str): - raise GuppyTypeError("Expected a string literal", tag) + raise GuppyTypeError(ExpectedError(tag, "a string literal")) if len(tag.value.encode("utf-8")) > TAG_MAX_LEN: - raise GuppyTypeError( - f"Tag is too long, limited to {TAG_MAX_LEN} bytes", tag - ) + err: Error = ResultChecker.TooLongError(tag) + err.add_sub_diagnostic(ResultChecker.TooLongError.Hint(None)) + raise GuppyTypeError(err) value, ty = ExprSynthesizer(self.ctx).synthesize(value) # We only allow numeric values or vectors of numeric values err = ResultChecker.InvalidError(value, ty) diff --git a/guppylang/tys/param.py b/guppylang/tys/param.py index f5186ba6..f56e4745 100644 --- a/guppylang/tys/param.py +++ b/guppylang/tys/param.py @@ -7,6 +7,8 @@ from typing_extensions import Self from guppylang.ast_util import AstNode +from guppylang.checker.errors.generic import ExpectedError +from guppylang.checker.errors.type_errors import TypeMismatchError from guppylang.error import GuppyError, GuppyTypeError, InternalGuppyError from guppylang.tys.arg import Argument, ConstArg, TypeArg from guppylang.tys.common import ToHugr @@ -90,14 +92,14 @@ def check_arg(self, arg: Argument, loc: AstNode | None = None) -> TypeArg: """ match arg: case ConstArg(const): - raise GuppyTypeError( - f"Expected a type, got value of type {const.ty}", loc - ) + err = ExpectedError(loc, "a type", got=f"value of type `{const.ty}`") + raise GuppyTypeError(err) case TypeArg(ty): if not self.can_be_linear and ty.linear: - raise GuppyTypeError( - f"Expected a non-linear type, got value of type {ty}", loc + err = ExpectedError( + loc, "a non-linear type", got=f"value of type `{ty}`" ) + raise GuppyTypeError(err) return arg def to_existential(self) -> tuple[Argument, ExistentialVar]: @@ -153,13 +155,14 @@ def check_arg(self, arg: Argument, loc: AstNode | None = None) -> ConstArg: case ConstArg(const): if const.ty != self.ty: raise GuppyTypeError( - f"Expected argument of type `{self.ty}`, got {const.ty}", loc + TypeMismatchError(loc, self.ty, const.ty, kind="argument") ) return arg - case TypeArg(): - raise GuppyTypeError( - f"Expected argument of type `{self.ty}`, got type", loc + case TypeArg(ty=ty): + err = ExpectedError( + loc, f"expression of type `{self.ty}`", got=f"type `{ty}`" ) + raise GuppyTypeError(err) def to_existential(self) -> tuple[Argument, ExistentialVar]: """Creates a fresh existential variable that can be instantiated for this diff --git a/tests/error/array_errors/new_array_cannot_infer.err b/tests/error/array_errors/new_array_cannot_infer.err index f5f9411c..a6e08506 100644 --- a/tests/error/array_errors/new_array_cannot_infer.err +++ b/tests/error/array_errors/new_array_cannot_infer.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Cannot infer type (at $FILE:13:9) + | +11 | @guppy(module) +12 | def main() -> None: +13 | xs = array() + | ^^^^^^^ Cannot infer the type of this array -11: @guppy(module) -12: def main() -> None: -13: xs = array() - ^^^^^^^ -GuppyTypeError: Cannot infer the array element type. Consider adding a type annotation. +Note: Consider adding a type annotation: `x: array[???] = ...` + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/array_errors/new_array_check_fail.err b/tests/error/array_errors/new_array_check_fail.err index 63978c5d..92502bce 100644 --- a/tests/error/array_errors/new_array_check_fail.err +++ b/tests/error/array_errors/new_array_check_fail.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Type mismatch (at $FILE:13:11) + | +11 | @guppy(module) +12 | def main() -> int: +13 | return array(1) + | ^^^^^^^^ Expected expression of type `int`, got `array[?T, ?n]` -11: @guppy(module) -12: def main() -> int: -13: return array(1) - ^^^^^^^^ -GuppyTypeError: Expected expression of type `int`, got `array` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/array_errors/new_array_wrong_length.err b/tests/error/array_errors/new_array_wrong_length.err index e0a577b5..2d303c4f 100644 --- a/tests/error/array_errors/new_array_wrong_length.err +++ b/tests/error/array_errors/new_array_wrong_length.err @@ -1,7 +1,9 @@ -Guppy compilation failed. Error in file $FILE:13 +Error: Type mismatch (at $FILE:13:11) + | +11 | @guppy(module) +12 | def main() -> array[int, 2]: +13 | return array(1, 2, 3) + | ^^^^^^^^^^^^^^ Expected expression of type `array[int, 2]`, got + | `array[int, 3]` -11: @guppy(module) -12: def main() -> array[int, 2]: -13: return array(1, 2, 3) - ^^^^^^^^^^^^^^ -GuppyTypeError: Expected expression of type `array[int, 2]`, got `array[int, 3]` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/result_tag_not_static.err b/tests/error/misc_errors/result_tag_not_static.err index d4dd0338..59e9fe12 100644 --- a/tests/error/misc_errors/result_tag_not_static.err +++ b/tests/error/misc_errors/result_tag_not_static.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Expected a string literal (at $FILE:7:11) + | +5 | @compile_guppy +6 | def foo(y: bool) -> None: +7 | result("foo" + "bar", y) + | ^^^^^^^^^^^^^ Expected a string literal -5: @compile_guppy -6: def foo(y: bool) -> None: -7: result("foo" + "bar", y) - ^^^^^^^^^^^^^ -GuppyTypeError: Expected a string literal +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/result_tag_not_str.err b/tests/error/misc_errors/result_tag_not_str.err index 575e6cf1..1e309747 100644 --- a/tests/error/misc_errors/result_tag_not_str.err +++ b/tests/error/misc_errors/result_tag_not_str.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:7 +Error: Expected a string literal (at $FILE:7:11) + | +5 | @compile_guppy +6 | def foo(x: int) -> None: +7 | result((), x) + | ^^ Expected a string literal -5: @compile_guppy -6: def foo(x: int) -> None: -7: result((), x) - ^^ -GuppyTypeError: Expected a string literal +Guppy compilation failed due to 1 previous error diff --git a/tests/error/misc_errors/result_tag_too_big.err b/tests/error/misc_errors/result_tag_too_big.err index 7626cab2..42485476 100644 --- a/tests/error/misc_errors/result_tag_too_big.err +++ b/tests/error/misc_errors/result_tag_too_big.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:11 +Error: Tag too long (at $FILE:11:11) + | + 9 | def foo(y: bool) -> None: +10 | # each tick or cross is 3 bytes. The cross sends the tag over the limit. +11 | result("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅❌", y) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Result tag is too long -9: def foo(y: bool) -> None: -10: # each tick or cross is 3 bytes. The cross sends the tag over the limit. -11: result("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅❌", y) - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -GuppyTypeError: Tag is too long, limited to 200 bytes +Note: Result tags are limited to 200 bytes + +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/arg_mismatch4.err b/tests/error/poly_errors/arg_mismatch4.err index 75ec9152..cfc54392 100644 --- a/tests/error/poly_errors/arg_mismatch4.err +++ b/tests/error/poly_errors/arg_mismatch4.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Expected expression of type `nat` (at $FILE:9:12) + | +7 | +8 | @guppy(module) +9 | def main(x: array[int, bool]) -> None: + | ^^^^^^^^^^^^^^^^ Expected expression of type `nat`, got type `bool` -7: @guppy(module) -8: def main(x: array[int, bool]) -> None: - ^^^^^^^^^^^^^^^^ -GuppyTypeError: Expected argument of type `nat`, got type +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/arg_mismatch5.err b/tests/error/poly_errors/arg_mismatch5.err index 6928c6d5..4e619bf1 100644 --- a/tests/error/poly_errors/arg_mismatch5.err +++ b/tests/error/poly_errors/arg_mismatch5.err @@ -1,6 +1,8 @@ -Guppy compilation failed. Error in file $FILE:9 +Error: Expected a type (at $FILE:9:12) + | +7 | +8 | @guppy(module) +9 | def main(x: list[42]) -> None: + | ^^^^^^^^ Expected a type, got value of type `nat` -7: @guppy(module) -8: def main(x: list[42]) -> None: - ^^^^^^^^ -GuppyTypeError: Expected a type, got value of type nat +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/non_linear1.err b/tests/error/poly_errors/non_linear1.err index 7c551eca..2af6b147 100644 --- a/tests/error/poly_errors/non_linear1.err +++ b/tests/error/poly_errors/non_linear1.err @@ -1,7 +1,9 @@ -Guppy compilation failed. Error in file $FILE:21 +Error: Not defined for linear argument (at $FILE:21:4) + | +19 | @guppy(module) +20 | def main(q: qubit) -> None: +21 | foo(q) + | ^^^^^^ Cannot instantiate non-linear type parameter `T` in type + | `forall T. T -> None` with linear type `qubit` -19: @guppy(module) -20: def main(q: qubit) -> None: -21: foo(q) - ^^^^^^ -GuppyTypeError: Cannot instantiate non-linear type variable `T` in type `forall T. T -> None` with linear type `qubit` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/non_linear2.err b/tests/error/poly_errors/non_linear2.err index 5c6827f2..744f8154 100644 --- a/tests/error/poly_errors/non_linear2.err +++ b/tests/error/poly_errors/non_linear2.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:23 +Error: Not defined for linear argument (at $FILE:23:4) + | +21 | @guppy(module) +22 | def main() -> None: +23 | foo(h) + | ^^^^^^ Cannot instantiate non-linear type parameter `T` in type + | `forall T. (T -> T) -> None` with linear type + | `qubit` -21: @guppy(module) -22: def main() -> None: -23: foo(h) - ^^^^^^ -GuppyTypeError: Cannot instantiate non-linear type variable `T` in type `forall T. (T -> T) -> None` with linear type `qubit` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/poly_errors/non_linear3.err b/tests/error/poly_errors/non_linear3.err index d70752e2..6dca5fdb 100644 --- a/tests/error/poly_errors/non_linear3.err +++ b/tests/error/poly_errors/non_linear3.err @@ -1,7 +1,10 @@ -Guppy compilation failed. Error in file $FILE:25 +Error: Not defined for linear argument (at $FILE:25:4) + | +23 | @guppy(module) +24 | def main() -> None: +25 | foo(h) + | ^^^^^^ Cannot instantiate non-linear type parameter `T` in type + | `forall T. (T -> None) -> None` with linear type + | `qubit` -23: @guppy(module) -24: def main() -> None: -25: foo(h) - ^^^^^^ -GuppyTypeError: Cannot instantiate non-linear type variable `T` in type `forall T. (T -> None) -> None` with linear type `qubit` +Guppy compilation failed due to 1 previous error diff --git a/tests/error/type_errors/call_not_function.err b/tests/error/type_errors/call_not_function.err index f04efe2b..dd9915ac 100644 --- a/tests/error/type_errors/call_not_function.err +++ b/tests/error/type_errors/call_not_function.err @@ -1,7 +1,8 @@ -Guppy compilation failed. Error in file $FILE:6 +Error: Not callable (at $FILE:6:11) + | +4 | @compile_guppy +5 | def foo(x: int) -> int: +6 | return x(42) + | ^ Expected a function, got expression of type `int` -4: @compile_guppy -5: def foo(x: int) -> int: -6: return x(42) - ^ -GuppyTypeError: Expected function type, got `int` +Guppy compilation failed due to 1 previous error From 09a658919f6e6ca3c8a2a330691b198fe8ad24ea Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Mon, 11 Nov 2024 16:28:35 +0000 Subject: [PATCH 20/20] refactor: Remove duplicate error class --- guppylang/checker/core.py | 14 -------------- guppylang/checker/expr_checker.py | 3 +-- guppylang/checker/func_checker.py | 3 ++- guppylang/checker/stmt_checker.py | 3 ++- 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/guppylang/checker/core.py b/guppylang/checker/core.py index 132e0a16..1c4b880f 100644 --- a/guppylang/checker/core.py +++ b/guppylang/checker/core.py @@ -7,7 +7,6 @@ from typing import ( TYPE_CHECKING, Any, - ClassVar, Generic, NamedTuple, TypeAlias, @@ -21,7 +20,6 @@ from guppylang.definition.common import DefId, Definition from guppylang.definition.ty import TypeDef from guppylang.definition.value import CallableDef -from guppylang.diagnostic import Error from guppylang.tys.builtin import ( array_type_def, bool_type_def, @@ -52,18 +50,6 @@ from guppylang.definition.struct import StructField -@dataclass(frozen=True) -class UnsupportedError(Error): - title: ClassVar[str] = "Unsupported" - things: str - singular: bool = False - - @property - def rendered_span_label(self) -> str: - is_are = "is" if self.singular else "are" - return f"{self.things} {is_are} not supported" - - #: A "place" is a description for a storage location of a local value that users #: can refer to in their program. #: diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index db5449a3..574cbae7 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -45,10 +45,9 @@ Locals, Place, SubscriptAccess, - UnsupportedError, Variable, ) -from guppylang.checker.errors.generic import ExpectedError +from guppylang.checker.errors.generic import ExpectedError, UnsupportedError from guppylang.checker.errors.linearity import LinearForBreakError from guppylang.checker.errors.py_errors import ( IllegalPyExpressionError, diff --git a/guppylang/checker/func_checker.py b/guppylang/checker/func_checker.py index 896defd2..f194b8f1 100644 --- a/guppylang/checker/func_checker.py +++ b/guppylang/checker/func_checker.py @@ -13,7 +13,8 @@ from guppylang.cfg.bb import BB from guppylang.cfg.builder import CFGBuilder from guppylang.checker.cfg_checker import CheckedCFG, check_cfg -from guppylang.checker.core import Context, Globals, Place, UnsupportedError, Variable +from guppylang.checker.core import Context, Globals, Place, Variable +from guppylang.checker.errors.generic import UnsupportedError from guppylang.definition.common import DefId from guppylang.diagnostic import Error, Help, Note from guppylang.error import GuppyError diff --git a/guppylang/checker/stmt_checker.py b/guppylang/checker/stmt_checker.py index 3f037f51..9928a01f 100644 --- a/guppylang/checker/stmt_checker.py +++ b/guppylang/checker/stmt_checker.py @@ -13,7 +13,8 @@ from guppylang.ast_util import AstVisitor, with_loc, with_type from guppylang.cfg.bb import BB, BBStatement -from guppylang.checker.core import Context, FieldAccess, UnsupportedError, Variable +from guppylang.checker.core import Context, FieldAccess, Variable +from guppylang.checker.errors.generic import UnsupportedError from guppylang.checker.errors.type_errors import ( AssignFieldTypeMismatchError, AssignNonPlaceHelp,