Repeat-Until-Success

These examples show two bounded repeat-until-success patterns written in Guppy.

They are useful because they demonstrate retry structures that stay within the subset that hugr-qir can lower: the retry budget is finite, the outer structure is known at compile time, and the resulting control flow is static from the backend’s point of view.

Bounded repeat-until-success

This version expands the retry structure at comptime and materializes fresh ancillas per attempt.

Source file: guppy_examples/repeat-until-success/supported/bounded-rus.py

from typing import no_type_check

from guppylang import guppy, qubit
from guppylang.std.builtins import result
from guppylang.std.quantum import cx, discard, measure, t, tdg, z
from guppylang.std.quantum.functional import h

N = 10


@guppy
@no_type_check
def rus_attempt(q: qubit) -> bool:
    # Same single-shot RUS body as the original example.
    a, b = h(qubit()), h(qubit())
    tdg(a)
    cx(b, a)
    t(a)
    if measure(h(a)):
        discard(b)
        return False
    t(q)
    z(q)
    cx(q, b)
    t(b)
    if measure(h(b)):
        z(q)
        return False
    return True


@guppy
@no_type_check
def rus_step(q: qubit, ok: bool, n: int) -> tuple[bool, int]:
    # The dynamic stop-or-retry decision lives in a regular Guppy function
    # because the outer retry structure below is expanded at comptime.
    if ok:
        return True, n
    return rus_attempt(q), n + 1


@guppy.comptime
@no_type_check
def main() -> None:
    # This version differs from an unbounded retry loop in three ways:
    # 1. The retry budget is finite (N attempts) rather than unbounded.
    # 2. The loop is unrolled at comptime so hugr-qir sees static control flow.
    # 3. Results expose both whether we succeeded and how many tries were used.
    # This two-stage retry body also materializes fresh ancillas per attempt in
    # QIR, unlike rus-flat-bounded.py which reuses 3 qubits.
    q = h(qubit())
    n = 0
    ok = False
    for _ in range(N):
        ok, n = rus_step(q, ok, n)
    result("attempts", n)
    result("success", ok)
    result("q", measure(q))

Compared to an unbounded retry loop, this version makes the retry count explicit and exposes both the number of attempts and the success flag as results.

Flat bounded repeat-until-success

This version allocates its ancillas once and reuses them across attempts.

Source file: guppy_examples/repeat-until-success/supported/rus-flat-bounded.py

from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import comptime, result
from guppylang.std.qsystem import measure_and_reset
from guppylang.std.quantum import discard, h, qubit, s, toffoli, z

N = 10


@no_type_check
def repeat_until_success(q: qubit, attempts: int @ comptime) -> tuple[bool, int]:
    # This differs from rus-flat-unbounded.py in three ways:
    # 1. The retry budget is finite (`attempts`) rather than unbounded.
    # 2. The loop bound is comptime-known so hugr-qir sees static control flow.
    # 3. Ancillas are allocated once and reused with measure_and_reset, so the
    #    lowered QIR only needs 3 qubits.
    success = False
    tries = 0
    a = qubit()
    b = qubit()
    for _ in range(attempts):
        success, tries = rus_step(q, a, b, success, tries)
    discard(a)
    discard(b)
    return success, tries


@guppy
@no_type_check
def rus_attempt(q: qubit, a: qubit, b: qubit) -> bool:
    h(a)
    h(b)
    toffoli(a, b, q)
    s(q)
    toffoli(a, b, q)
    h(a)
    h(b)
    c0 = measure_and_reset(a)
    c1 = measure_and_reset(b)
    if not (c0 | c1):
        return True
    z(q)
    return False


@guppy
@no_type_check
def rus_step(
    q: qubit, a: qubit, b: qubit, success: bool, tries: int
) -> tuple[bool, int]:
    # The dynamic stop-or-retry decision lives in regular Guppy code because
    # the outer retry structure below is expanded at comptime. Returning early
    # on success keeps later unrolled attempts out of the runtime path.
    if success:
        return True, tries
    if rus_attempt(q, a, b):
        return True, tries
    return False, tries + 1


@guppy.comptime
@no_type_check
def main() -> None:
    # The unbounded version retries with `while True`. Here the retry budget is
    # fixed at compile time and the final outputs are emitted once after the
    # loop. `attempts` is the number of failed attempts before success, or `N`
    # if all attempts fail.
    q = qubit()
    success, attempts = repeat_until_success(q, comptime(N))
    result("success", success)
    result("attempts", attempts)
    discard(q)

Compared to the first version above, this variant is useful for understanding a lower-qubit implementation of the same retry pattern.