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: add panic builtin function #757

Merged
merged 4 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
15 changes: 15 additions & 0 deletions guppylang/compiler/expr_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
GlobalName,
InoutReturnSentinel,
LocalCall,
PanicExpr,
PartialApply,
PlaceNode,
ResultExpr,
Expand All @@ -53,6 +54,7 @@
from guppylang.std._internal.compiler.list import (
list_new,
)
from guppylang.std._internal.compiler.prelude import build_error, build_panic
from guppylang.tys.arg import Argument
from guppylang.tys.builtin import (
get_element_type,
Expand Down Expand Up @@ -473,6 +475,19 @@ def visit_ResultExpr(self, node: ResultExpr) -> Wire:
self.builder.add_op(op, self.visit(node.value))
return self._pack_returns([], NoneType())

def visit_PanicExpr(self, node: PanicExpr) -> Wire:
err = build_error(self.builder, 1, node.msg)
in_tys = [get_type(e).to_hugr() for e in node.values]
out_tys = [ty.to_hugr() for ty in type_to_row(get_type(node))]
outs = build_panic(
self.builder,
in_tys,
out_tys,
err,
*(self.visit(e) for e in node.values),
).outputs()
return self._pack_returns(list(outs), get_type(node))

def visit_DesugaredListComp(self, node: DesugaredListComp) -> Wire:
# Make up a name for the list under construction and bind it to an empty list
list_ty = get_type(node)
Expand Down
9 changes: 9 additions & 0 deletions guppylang/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ class ResultExpr(ast.expr):
_fields = ("value", "base_ty", "array_len", "tag")


class PanicExpr(ast.expr):
"""A `panic(msg, *args)` expression."""

msg: str
values: list[ast.expr]

_fields = ("msg", "values")


class InoutReturnSentinel(ast.expr):
"""An invisible expression corresponding to an implicit use of borrowed vars
whenever a function returns."""
Expand Down
37 changes: 37 additions & 0 deletions guppylang/std/_internal/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
GenericParamValue,
GlobalCall,
MakeIter,
PanicExpr,
ResultExpr,
)
from guppylang.tys.arg import ConstArg, TypeArg
Expand Down Expand Up @@ -355,6 +356,42 @@ def _is_numeric_or_bool_type(ty: Type) -> bool:
return isinstance(ty, NumericType) or is_bool_type(ty)


class PanicChecker(CustomCallChecker):
"""Call checker for the `panic` function."""

@dataclass(frozen=True)
class NoMessageError(Error):
title: ClassVar[str] = "No panic message"
span_label: ClassVar[str] = "Missing message argument to panic call"

@dataclass(frozen=True)
class Suggestion(Note):
message: ClassVar[str] = 'Add a message: `panic("message")`'

def synthesize(self, args: list[ast.expr]) -> tuple[ast.expr, Type]:
match args:
case []:
err = PanicChecker.NoMessageError(self.node)
err.add_sub_diagnostic(PanicChecker.NoMessageError.Suggestion(None))
raise GuppyTypeError(err)
case [msg, *rest]:
if not isinstance(msg, ast.Constant) or not isinstance(msg.value, str):
raise GuppyTypeError(ExpectedError(msg, "a string literal"))

vals = [ExprSynthesizer(self.ctx).synthesize(val)[0] for val in rest]
node = PanicExpr(msg.value, vals)
return with_loc(self.node, node), NoneType()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should panic be generic over the type it returns? This would require also implementing check to infer the return type.

Imo the proper way to implement this would be to use a bottom return type like Python's Never or NoReturn for panic, but that's too much for this PR

Copy link
Member Author

Choose a reason for hiding this comment

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

I couldn't think of a good reason for panic returning anything other than None, do you think it is important?

Copy link
Collaborator

Choose a reason for hiding this comment

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

For example, it's currently not possible to make the type checker understand that panics are an early exit. So, say you want to implement Option.unwrap yourself, then you still need to explicitly return something in the error case:

def unwrap(opt: Option[T]) -> T:
    match opt:
        case Some(x): return x
        case Nothing: return panic("Is None")

case args:
return assert_never(args) # type: ignore[arg-type]

def check(self, args: list[ast.expr], ty: Type) -> tuple[ast.expr, Subst]:
# Panic may return any type, so we don't have to check anything. Consequently
# we also can't infer anything in the expected type, so we always return an
# empty substitution
expr, _ = self.synthesize(args)
return expr, {}


class RangeChecker(CustomCallChecker):
"""Call checker for the `range` function."""

Expand Down
5 changes: 3 additions & 2 deletions guppylang/std/_internal/compiler/prelude.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import hugr.std.collections
import hugr.std.int
import hugr.std.prelude
from hugr import Node, Wire, ops
from hugr import tys as ht
from hugr import val as hv
Expand Down Expand Up @@ -63,7 +64,7 @@ def panic(inputs: list[ht.Type], outputs: list[ht.Type]) -> ops.ExtOp:


def build_panic(
builder: DfBase[ops.Case],
builder: DfBase[P],
in_tys: ht.TypeRow,
out_tys: ht.TypeRow,
err: Wire,
Expand All @@ -74,7 +75,7 @@ def build_panic(
return builder.add_op(op, err, *args)


def build_error(builder: DfBase[ops.Case], signal: int, msg: str) -> Wire:
def build_error(builder: DfBase[P], signal: int, msg: str) -> Wire:
"""Constructs and loads a static error value."""
val = ErrorVal(signal, msg)
return builder.load(builder.add_const(val))
Expand Down
5 changes: 5 additions & 0 deletions guppylang/std/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CallableChecker,
DunderChecker,
NewArrayChecker,
PanicChecker,
RangeChecker,
ResultChecker,
ReversingChecker,
Expand Down Expand Up @@ -642,6 +643,10 @@ def __iter__(self: "SizedIter[L, n]" @ owned) -> "SizedIter[L, n]": # type: ign
def result(tag, value): ...


@guppy.custom(checker=PanicChecker(), higher_order_value=False)
def panic(msg, *args): ...


@guppy.custom(checker=DunderChecker("__abs__"), higher_order_value=False)
def abs(x): ...

Expand Down
10 changes: 10 additions & 0 deletions tests/error/misc_errors/panic_msg_empty.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Error: No panic message (at $FILE:7:4)
|
5 | @compile_guppy
6 | def foo(x: int) -> None:
7 | panic()
| ^^^^^^^ Missing message argument to panic call

Note: Add a message: `panic("message")`

Guppy compilation failed due to 1 previous error
7 changes: 7 additions & 0 deletions tests/error/misc_errors/panic_msg_empty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from guppylang.std.builtins import panic
from tests.util import compile_guppy


@compile_guppy
def foo(x: int) -> None:
panic()
8 changes: 8 additions & 0 deletions tests/error/misc_errors/panic_msg_not_str.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Error: Expected a string literal (at $FILE:7:10)
|
5 | @compile_guppy
6 | def foo(x: int) -> None:
7 | panic((), x)
| ^^ Expected a string literal

Guppy compilation failed due to 1 previous error
7 changes: 7 additions & 0 deletions tests/error/misc_errors/panic_msg_not_str.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from guppylang.std.builtins import panic
from tests.util import compile_guppy


@compile_guppy
def foo(x: int) -> None:
panic((), x)
8 changes: 8 additions & 0 deletions tests/error/misc_errors/panic_tag_not_static.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Error: Expected a string literal (at $FILE:7:10)
|
5 | @compile_guppy
6 | def foo(y: bool) -> None:
7 | panic("foo" + "bar", y)
| ^^^^^^^^^^^^^ Expected a string literal

Guppy compilation failed due to 1 previous error
7 changes: 7 additions & 0 deletions tests/error/misc_errors/panic_tag_not_static.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from guppylang.std.builtins import panic
from tests.util import compile_guppy


@compile_guppy
def foo(y: bool) -> None:
panic("foo" + "bar", y)
38 changes: 38 additions & 0 deletions tests/integration/test_panic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from guppylang import GuppyModule, guppy
from guppylang.std.builtins import panic
from tests.util import compile_guppy


def test_basic(validate):
@compile_guppy
def main() -> None:
panic("I panicked!")

validate(main)


def test_discard(validate):
@compile_guppy
def main() -> None:
a = 1 + 2
panic("I panicked!", False, a)

validate(main)


def test_value(validate):
module = GuppyModule("test")

@guppy(module)
def foo() -> int:
return panic("I panicked!")

@guppy(module)
def bar() -> tuple[int, float]:
return panic("I panicked!")

@guppy(module)
def baz() -> None:
return panic("I panicked!")

validate(module.compile())
13 changes: 13 additions & 0 deletions tests/integration/test_quantum.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Various tests for the functions defined in `guppylang.prelude.quantum`."""

from typing import no_type_check
from hugr.package import ModulePointer

import guppylang.decorator
Expand Down Expand Up @@ -154,3 +155,15 @@ def test() -> None:
discard_array(qs)

validate(test)


def test_panic_discard(validate):
"""Panic while discarding qubit."""

@compile_quantum_guppy
@no_type_check
def test() -> None:
q = qubit()
panic("I panicked!", q)

validate(test)
Loading