Function flags

For a control block, calls to classical functions need no flag: they are evaluated normally and are not controlled. A call involving qubits must instead be marked controllable=True.

import math

from guppylang import array, guppy
from guppylang.std.builtins import control, dagger, nat, output
from guppylang.std.quantum import angle, h, measure, qubit, rx, s, x, z

@guppy
def classical_step(n: int) -> int:
    q = qubit()
    h(q)
    if measure(q):
        n = n + 1
    return n

@guppy
def controlled_classical_call(c: qubit, q: qubit) -> None:
    with control(c):
        denominator = classical_step(2)
        rx(q, angle(1 / denominator))

controlled_classical_call.check()

This is different for dagger blocks: since they change the order of quantum operations, every called function, including a classical one, must be daggerable=True; this prevents a function from silently allocating or measuring a qubit.

For example, an unflagged classical call is rejected in a dagger block:

@guppy
def classical_helper() -> int:
    return 0

@guppy
def unflagged_call_in_dagger(qs: array[qubit, 2]) -> None:
    a = angle(1 / 4)
    with dagger:
        i = classical_helper()
        rx(qs[i], a)

unflagged_call_in_dagger.check()
Error: Dagger constraint violation (at <In[2]>:9:12)
  | 
7 |     a = angle(1 / 4)
8 |     with dagger:
9 |         i = classical_helper()
  |             ^^^^^^^^^^^^^^^^^^ This function cannot be called in a daggerable context

Help: Consider adding the flag `(daggerable=True)` to the decorator of the
function `classical_helper`

Guppy compilation failed due to 1 previous error

Marking it daggerable=True makes the call valid:

@guppy(daggerable=True)
def daggerable_classical_helper() -> int:
    return 0

@guppy
def flagged_call_in_dagger(qs: array[qubit, 2]) -> None:
    a = angle(1 / 4)
    with dagger:
        i = daggerable_classical_helper()
        rx(qs[i], a)

flagged_call_in_dagger.check()

A call valid inside both control and dagger must be unitary=True. Declaring a function with @guppy(unitary=True) or with @guppy(daggerable=True, controllable=True) is equivalent.

@guppy(unitary=True)
def unitary_gate(q: qubit) -> None:
    h(q)

@guppy(daggerable=True, controllable=True)
def dagger_and_controllable_gate(q: qubit) -> None:
    h(q)

@guppy
def explicit_controlled_dagger_call(c: qubit, q: qubit) -> None:
    with control(c), dagger:
        unitary_gate(q)
        dagger_and_controllable_gate(q)

explicit_controlled_dagger_call.check()

When a flag is declared on a function, Guppy verifies the function body, ensuring that it adheres to the constraints imposed by the flag.

@guppy(daggerable=True)
def flagged_branch(q: qubit, flag: bool) -> None:
    if flag:
        h(q)

flagged_branch.check()
Error: Invalid expression in dagger (at <In[5]>:3:4)
  | 
1 | @guppy(daggerable=True)
2 | def flagged_branch(q: qubit, flag: bool) -> None:
3 |     if flag:
  |     ^^^^^^^^
4 |         h(q)
  | ^^^^^^^^^^^^ Branch found in a dagger context

Help: The function `flagged_branch` is declared with the unitary flag:
`daggerable=True`. Thus dagger constraints apply to its body.

Help: Control flow statements (e.g. loops and branches) are not allowed in
daggered contexts.

Guppy compilation failed due to 1 previous error
@guppy(controllable=True)
def flagged_allocation() -> None:
    q = qubit()
    measure(q)

flagged_allocation.check()
Error: Control constraint violation (at <In[6]>:3:8)
  | 
1 | @guppy(controllable=True)
2 | def flagged_allocation() -> None:
3 |     q = qubit()
  |         ^^^^^^^ This function cannot be called in a controllable context

Note: The function allocates qubits, which is not allowed in a controllable
context

Guppy compilation failed due to 1 previous error
@guppy(unitary=True)
def flagged_loop(q: qubit) -> None:
    for _ in range(2):
        h(q)

flagged_loop.check()
Error: Invalid expression in dagger (at <In[7]>:3:4)
  | 
1 | @guppy(unitary=True)
2 | def flagged_loop(q: qubit) -> None:
3 |     for _ in range(2):
  |     ^^^^^^^^^^^^^^^^^^
4 |         h(q)
  | ^^^^^^^^^^^^ Loop found in a dagger context

Help: The function `flagged_loop` is declared with the unitary flag:
`unitary=True`. Thus dagger constraints apply to its body.

Help: Control flow statements (e.g. loops and branches) are not allowed in
daggered contexts.

Guppy compilation failed due to 1 previous error

The unitary flag also requires dagger constraints, so every classical function called inside a unitary function must be at least daggerable=True.

@guppy(daggerable=True)
def get_index() -> int:
    return 0

@guppy(unitary=True)
def unitary_fun(qs: array[qubit, 2], a: angle) -> None:
    i = get_index()
    rx(qs[i], a)

unitary_fun.check()

Function flags with compile-time functions

Function flags can also be used with guppy.comptime functions. Here, since the control flow is evaluated at compile time, no restrictions are enforced and the function can be called inside a dagger block.

@guppy.comptime(unitary=True)
def choose_gate(q: qubit, flag: bool) -> None:
    if flag:
        h(q)
    else:
        x(q)

@guppy
def modified_comptime_call(c: qubit, q: qubit) -> None:
    with control(c), dagger:
        choose_gate(q, True)

modified_comptime_call.check()

Qubit allocation or measurement remains forbidden in a flagged compile-time function:

@guppy.comptime(daggerable=True)
def allocating_comptime_function() -> None:
    q = qubit()
    h(q)
    measure(q)

allocating_comptime_function.compile()
---------------------------------------------------------------------------
GuppyComptimeError                        Traceback (most recent call last)
    [... skipping hidden 1 frame]

Cell In[10], line 7
      4     h(q)
      5     measure(q)
----> 7 allocating_comptime_function.compile()

Cell In[10], line 3, in allocating_comptime_function()
      1 @guppy.comptime(daggerable=True)
      2 def allocating_comptime_function() -> None:
----> 3     q = qubit()
      4     h(q)
      5     measure(q)

GuppyComptimeError: Dagger constraint violation: This function cannot be called in a daggerable context

Higher-order functions

We can use modifiers also with higher-order functions. Higher-order functions are functions that take other functions as arguments. The modifier applies to the body of the higher-order function, so it can be used to control or dagger the function argument. To be able to modify the function argument, we need special function types that ensure that the function argument can be modified. These types are Controllable, Daggerable, and Unitary. They describe functions that can be called in a control block, a dagger block, or both, respectively. This lets the type checker verify the required capability when the function is passed.

from guppylang.std.builtins import Controllable, Daggerable, Unitary

@guppy
def apply_controlled(op: Controllable[[qubit], None], c: qubit, q: qubit) -> None:
    with control(c):
        op(q)

@guppy
def apply_dagger(op: Daggerable[[qubit], None], q: qubit) -> None:
    with dagger:
        op(q)

@guppy
def apply_controlled_dagger(op: Unitary[[qubit], None], c: qubit, q: qubit) -> None:
    with control(c), dagger:
        op(q)

@guppy(controllable=True)
def controllable_x(q: qubit) -> None:
    x(q)

@guppy(daggerable=True)
def daggerable_s(q: qubit) -> None:
    s(q)

@guppy(unitary=True)
def unitary_h(q: qubit) -> None:
    h(q)

@guppy
def use_modifiable_functions(c: qubit, q: qubit) -> None:
    apply_controlled(controllable_x, c, q)
    apply_dagger(daggerable_s, q)
    apply_controlled_dagger(unitary_h, c, q)

use_modifiable_functions.check()

The higher-order function itself can be called inside a modifier block. It must declare the capability required by that block; its function argument then carries the same requirement.

@guppy(unitary=True)
def apply_unitary(op: Unitary[[qubit], None], q: qubit) -> None:
    op(q)

@guppy
def modify_higher_order_call(c: qubit, q: qubit) -> None:
    with control(c), dagger:
        apply_unitary(unitary_h, q)

modify_higher_order_call.check()

The annotation is checked at the call site. A function with insufficient capabilities is rejected:

@guppy(daggerable=True)
def only_daggerable(q: qubit) -> None:
    s(q)

@guppy
def need_controllable(c: qubit, q: qubit) -> None:
    apply_controlled(only_daggerable, c, q)

need_controllable.check()
Error: Missing protocol implementation (at <In[13]>:7:21)
  | 
5 | @guppy
6 | def need_controllable(c: qubit, q: qubit) -> None:
7 |     apply_controlled(only_daggerable, c, q)
  |                      ^^^^^^^^^^^^^^^ Type `def only_daggerable(q: qubit) -> None` does not
  |                                      implement protocol `Controllable[[qubit], None]`

Note: Function `only_daggerable` is only declared as `daggerable=True`

Guppy compilation failed due to 1 previous error
@guppy(controllable=True)
def only_controllable(q: qubit) -> None:
    x(q)

@guppy
def need_daggerable(q: qubit) -> None:
    apply_dagger(only_controllable, q)

need_daggerable.check()
Error: Missing protocol implementation (at <In[14]>:7:17)
  | 
5 | @guppy
6 | def need_daggerable(q: qubit) -> None:
7 |     apply_dagger(only_controllable, q)
  |                  ^^^^^^^^^^^^^^^^^ Type `def only_controllable(q: qubit) -> None` does not
  |                                    implement protocol `Daggerable[[qubit], None]`

Note: Function `only_controllable` is only declared as `controllable=True`

Guppy compilation failed due to 1 previous error
@guppy(daggerable=True)
def not_unitary(q: qubit) -> None:
    s(q)

@guppy
def need_unitary(c: qubit, q: qubit) -> None:
    apply_controlled_dagger(not_unitary, c, q)

need_unitary.check()
Error: Missing protocol implementation (at <In[15]>:7:28)
  | 
5 | @guppy
6 | def need_unitary(c: qubit, q: qubit) -> None:
7 |     apply_controlled_dagger(not_unitary, c, q)
  |                             ^^^^^^^^^^^ Type `def not_unitary(q: qubit) -> None` does not implement
  |                                         protocol `Unitary[[qubit], None]`

Note: Function `not_unitary` is only declared as `daggerable=True`

Guppy compilation failed due to 1 previous error

Loading from pytket

Loaded pytket circuits infer their modifier capabilities from their operations. A circuit containing only unitary gates is unitary, so it can be controlled and daggered.

from pytket import Circuit

circuit = Circuit(1)
circuit.H(0)
hadamard = guppy.load_pytket("hadamard", circuit, use_arrays=False)

@guppy
def controlled_inverse_circuit(c: qubit, q: qubit) -> None:
    with control(c), dagger:
        hadamard(q)

controlled_inverse_circuit.check()

If the circuit contains measurements, resets, or discards, it is not unitary and cannot be controlled or daggered.

from pytket import Circuit

circuit = Circuit(1)
circuit.H(0)
circuit.measure_all()
measured_circuit = guppy.load_pytket("measured_circuit", circuit, use_arrays=False)

@guppy
def modify_measured_circuit(c: qubit, q: qubit) -> None:
    with control(c), dagger:
        measured_circuit(q)

modify_measured_circuit.check()
Error: Unitary constraint violation (at <In[17]>:11:8)
   | 
 9 | def modify_measured_circuit(c: qubit, q: qubit) -> None:
10 |     with control(c), dagger:
11 |         measured_circuit(q)
   |         ^^^^^^^^^^^^^^^^^^^ This function cannot be called in a unitary context

Help: The function `measured_circuit` corresponds to a pytket circuit containing
a non unitary operation, which is not allowed in a unitary context.

Guppy compilation failed due to 1 previous error