Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Update remaining code to use new diagnostics #606

Merged
merged 14 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions guppylang/cfg/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand Down Expand Up @@ -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."""

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down
Empty file.
45 changes: 45 additions & 0 deletions guppylang/checker/errors/generic.py
Original file line number Diff line number Diff line change
@@ -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 ""
15 changes: 8 additions & 7 deletions guppylang/compiler/expr_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
19 changes: 7 additions & 12 deletions guppylang/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -33,7 +33,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,
Expand Down Expand Up @@ -149,7 +149,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")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Guppy errors are now reserved for things that go wrong during compilation. This error is thrown immediately when the user calls the decorator, so we shouldn't use GuppyError

Same for other errors in this file


# Jupyter notebook cells all get different dummy filenames. However,
# we want the whole notebook to correspond to a single implicit
Expand All @@ -173,7 +173,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
Expand Down Expand Up @@ -310,11 +310,6 @@ def custom(

def dec(f: PyFunc) -> RawCustomFunctionDef:
func_ast, docstring = parse_py_func(f, self._sources)
if not has_empty_body(func_ast):
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's going on here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I moved this check to RawCustomFunctionDef.check since it doesn't really belong into the decorator imo

raise GuppyError(
"Body of custom function declaration must be empty",
func_ast.body[0],
)
call_checker = checker or DefaultCallChecker()
func = RawCustomFunctionDef(
DefId.fresh(mod),
Expand Down Expand Up @@ -432,7 +427,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
Expand All @@ -453,7 +448,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
Expand Down Expand Up @@ -482,7 +477,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
Expand Down
10 changes: 10 additions & 0 deletions guppylang/definition/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from hugr.build.dfg import DefinitionBuilder, OpVar

from guppylang.diagnostic import Fatal
from guppylang.span import SourceMap

if TYPE_CHECKING:
Expand Down Expand Up @@ -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
49 changes: 39 additions & 10 deletions guppylang/definition/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
from abc import ABC, abstractmethod
from collections.abc import Callable, Sequence
from dataclasses import dataclass, field
from typing import 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
Expand All @@ -28,6 +30,36 @@
)


@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)`"
)


@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):
"""A raw custom function definition provided by the user.
Expand Down Expand Up @@ -69,6 +101,8 @@ 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.
"""
if not has_empty_body(self.defined_at):
raise GuppyError(BodyNotEmptyError(self.defined_at.body[0], self.name))
sig = self._get_signature(globals)
ty = sig or FunctionType([], NoneType())
return CustomFunctionDef(
Expand Down Expand Up @@ -122,11 +156,9 @@ def _get_signature(self, globals: Globals) -> FunctionType | None:
)

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,
)
err = NoSignatureError(self.defined_at, self.name)
err.add_sub_diagnostic(NoSignatureError.Suggestion(None))
raise GuppyError(err)

if requires_type_annotation:
return check_signature(self.defined_at, globals)
Expand Down Expand Up @@ -196,10 +228,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
Expand Down
Loading
Loading