Skip to content

Commit

Permalink
feat: add panic builtin function (#757)
Browse files Browse the repository at this point in the history
Closes #756

---------

Co-authored-by: Mark Koch <[email protected]>
Co-authored-by: Mark Koch <[email protected]>
  • Loading branch information
3 people authored Jan 8, 2025
1 parent 70c8fcf commit 4ae3032
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 2 deletions.
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()
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
15 changes: 15 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 @@ -651,6 +652,20 @@ 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):
"""Panic, throwing an error with the given message, and immediately exit the
program.
Return type is arbitrary, as this function never returns.
Args:
msg: The message to display. Must be a string literal.
args: Arbitrary extra inputs, will not affect the message. Only useful for
consuming linear values.
"""


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

0 comments on commit 4ae3032

Please sign in to comment.