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/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 0983a31b..40d6e637 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