Arrays

Fixed-size arrays are supported in both @guppy and @guppy.comptime code. This includes arrays created from Python lists.

H-Series requires the exact qubit used by every quantum operation to be known when the program is compiled. hugr-qir makes a best effort to simplify array indices and fully expand loops. Compilation fails if a qubit index cannot be made static or if a loop cannot be fully expanded.

This means runtime indexing sometimes works, but is not guaranteed to work just because the array has a fixed size.

Supported: runtime arrays and array parameters

Source file: guppy_examples/guppy-features/supported/runtime-array.py

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output, qubit
from guppylang.std.quantum import collect_measurements, cx, h, measure_array


@guppy
@no_type_check
def create_steane() -> array[qubit, 7]:
    return array(qubit() for _ in range(7))


@guppy
@no_type_check
def steane_h(stq: array[qubit, 7]) -> None:
    for i in range(7):
        h(stq[i])


@guppy
@no_type_check
def steane_cx(
    stq1: array[qubit, 7],
    stq2: array[qubit, 7],
) -> None:
    for i in range(7):
        cx(stq1[i], stq2[i])


@guppy
@no_type_check
def main() -> None:
    steane_q1 = create_steane()
    steane_q2 = create_steane()
    steane_h(steane_q1)
    steane_h(steane_q2)
    steane_cx(steane_q1, steane_q2)
    output("steane_q1", collect_measurements(measure_array(steane_q1)))
    output("steane_q2", collect_measurements(measure_array(steane_q2)))


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

This example creates qubit arrays in an ordinary @guppy function, passes them to other Guppy functions, and accesses them in fixed-size loops.

Supported: fixed-size iteration

Source file: guppy_examples/guppy-features/supported/array-static-full-iteration.py

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, qubit
from guppylang.std.quantum import h, measure


@guppy
@no_type_check
def main() -> None:
    # A fixed-size array iteration has a statically-known trip count. The array
    # is consumed by the loop, so no dynamic array cleanup is required.
    qbs = array(qubit() for _ in range(4))
    for qb in qbs:
        h(qb)
        measure(qb).read()


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Iterating over an entire fixed-size array can be fully expanded during compilation.

Supported: finite runtime selection

Source file: guppy_examples/guppy-features/supported/array-branch-selected-borrow.py

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output, qubit
from guppylang.std.quantum import h, measure


@guppy
@no_type_check
def main() -> None:
    # A Boolean runtime selector can be lowered to two control-flow branches,
    # each of which uses a static qubit resource pointer.
    selector = qubit()
    h(selector)
    index = int(measure(selector).read())

    qbs = array(qubit() for _ in range(2))
    h(qbs[index])

    qb0, qb1 = qbs
    output("qb0", measure(qb0).read())
    output("qb1", measure(qb1).read())


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

The index in this example depends on a measurement, but can only be zero or one. It can therefore be compiled into two branches, each using a known qubit.

Supported: bounded data-dependent exit

Source file: guppy_examples/guppy-features/supported/array-data-dependent-early-exit.py

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output, qubit
from guppylang.std.quantum import h, measure, x


@guppy
@no_type_check
def main() -> None:
    selector = qubit()
    h(selector)
    stop_early = measure(selector).read()
    target = qubit()

    # Although the exact exit iteration depends on a measurement, the fixed
    # array size gives LLVM a finite upper bound that it can expand into
    # guarded, acyclic control flow.
    values = array(i + 10 for i in range(4))
    for _ in values:
        x(target)
        if stop_early:
            break

    output("target", measure(target).read())


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Although a measurement determines when this loop exits, its maximum number of iterations is fixed. The loop can be fully expanded into conditional steps.

Supported: implicit discard

Source file: guppy_examples/guppy-features/supported/array-implicit-discard.py

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output


@guppy
@no_type_check
def main() -> None:
    # Copyable, droppable elements allow the remaining array to be discarded
    # implicitly after a statically-indexed access.
    values = array(i + 10 for i in range(4))
    output("value", values[2])


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Arrays of copyable, droppable values can be discarded after use.

Supported: compile-time arrays

Source file: guppy_examples/guppy-features/supported/comptime-array.py

from __future__ import annotations

import sys
from typing import TYPE_CHECKING, no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output, qubit
from guppylang.std.quantum import cx, measure, x

if TYPE_CHECKING:
    from guppylang.std.lang import owned


@no_type_check
def create_steane(name: str) -> tuple[str, array[qubit, 7]]:
    return name, [qubit() for _ in range(7)]


def steane_x(stq: list[qubit]) -> None:
    for q in stq:
        x(q)


def steane_cx(
    stq1: list[qubit],
    stq2: list[qubit],
) -> None:
    num_q = len(stq1)
    for i in range(num_q):
        cx(stq1[i], stq2[i])


@no_type_check
def steane_measure(
    stq: list[qubit] @ owned,
) -> tuple[bool, bool, bool, bool, bool, bool, bool]:
    return tuple([measure(stq[i]).read() for i in range(7)])


@no_type_check
def steane_measure_result(
    stq1: tuple[str, list[qubit]] @ owned,
) -> None:
    name, qbs = stq1
    res = steane_measure(qbs)
    for i in range(len(res)):
        output(f"{name}_{i}", res[i])


@guppy.comptime
@no_type_check
def main() -> None:
    steane_q1 = create_steane("q1")
    steane_q2 = create_steane("q2")
    steane_x(steane_q1[1])
    steane_x(steane_q2[1])
    steane_cx(steane_q1[1], steane_q2[1])
    steane_measure_result(steane_q1)
    steane_measure_result(steane_q2)


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

@guppy.comptime is useful when all array indices and loop bounds can be decided during compilation. Copying and starred unpacking at compile time are demonstrated separately:

from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output


@guppy.comptime
@no_type_check
def main() -> None:
    values = array(1, 2, 3, 4)
    copied = values.copy()
    first, *tail = copied
    values[0] = 10

    output("array_first", first)
    output("array_tail_1", tail[1])
    output("array_mutated", values[0])

Supported: measurement and discard helpers

measure_array and discard_array follow the same array rules.

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output, qubit
from guppylang.std.quantum import collect_measurements, cx, h, measure_array


@guppy.comptime
@no_type_check
def main() -> None:
    qbs = array(qubit() for _ in range(8))
    for i in range(8):
        if i % 2 == 0:
            h(qbs[i])
        else:
            cx(qbs[i - 1], qbs[i])

    results = collect_measurements(measure_array(qbs))
    output("qbs", results)


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())
import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output, qubit
from guppylang.std.quantum import cx, discard_array, h, measure


@guppy.comptime
@no_type_check
def main() -> None:
    qbs = array(qubit() for _ in range(8))
    for i in range(8):
        if i % 2 == 0:
            h(qbs[i])
        else:
            cx(qbs[i - 1], qbs[i])

    measure_q, *discard_qs = qbs

    output("qb0", measure(measure_q).read())
    discard_array(discard_qs)


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Unsupported: unresolved runtime index

Source file: guppy_examples/guppy-features/unsupported/array-runtime-index.py

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import array, output, qubit
from guppylang.std.qsystem.random import RNG
from guppylang.std.quantum import measure, x


@guppy
@no_type_check
def main() -> None:
    # Unlike the supported Boolean-selector example, optimization does not
    # turn this runtime index into statically addressed QIR operations.
    rng = RNG(11)
    index = rng.random_int_bounded(4)
    rng.discard()

    qbs = array(qubit() for _ in range(4))
    x(qbs[index])

    qb0, qb1, qb2, qb3 = qbs
    output("qb0", measure(qb0).read())
    output("qb1", measure(qb1).read())
    output("qb2", measure(qb2).read())
    output("qb3", measure(qb3).read())


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Here an RNG selects the qubit. The compiler cannot determine a fixed qubit for the x operation, so compilation fails.

Expected error:

Compilation failed: Program may panic: Index out of bounds. Runtime panics are unsupported on H-Series.

Array-backed collections

Fixed sequences of Stack and Queue operations are supported when the compiler can completely remove the collection’s internal storage and control flow.

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import output
from guppylang.std.collections.stack import empty_stack


@guppy
@no_type_check
def main() -> None:
    # This fixed sequence can be fully simplified during QIR compilation.
    stack = empty_stack[int, 4]()
    stack.push(3)
    stack.push(5)
    output("stack_len", len(stack))
    output("stack_top", stack.pop())
    output("stack_next", stack.pop())
    stack.discard_empty()


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())
import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import output
from guppylang.std.collections.queue import empty_queue


@guppy
@no_type_check
def main() -> None:
    # This fixed sequence can be fully simplified during QIR compilation.
    queue = empty_queue[int, 4]()
    queue.push(3)
    queue.push(5)
    output("queue_len", len(queue))
    output("queue_front", queue.pop())
    output("queue_next", queue.pop())
    queue.discard_empty()


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Unsupported: runtime-dependent collection state

Collection state that depends on runtime values may leave internal storage or dynamic addressing in the generated program. These examples cannot be reduced to the static operations required by H-Series QIR.

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import output, qubit
from guppylang.std.collections.stack import empty_stack
from guppylang.std.qsystem.utils import get_current_shot
from guppylang.std.quantum import discard, measure, x


@guppy
@no_type_check
def main() -> None:
    # The stack's top qubit depends on a runtime value, so the qubit used by the
    # x gate cannot be reduced to a single static address.
    stack = empty_stack[qubit, 2]()
    stack.push(qubit())
    second = qubit()
    push_second = get_current_shot() % 2 == 0
    if push_second:
        stack.push(second)
    else:
        discard(second)

    top = stack.pop()
    x(top)
    output("stack_top", measure(top).read())
    if push_second:
        discard(stack.pop())
    stack.discard_empty()


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())
import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import output
from guppylang.std.collections.queue import empty_queue
from guppylang.std.qsystem.utils import get_current_shot


@guppy
@no_type_check
def main() -> None:
    # The queue's size depends on a runtime value, so its backing storage cannot
    # be fully removed during QIR compilation.
    queue = empty_queue[int, 4]()
    queue.push(3)
    push_second = get_current_shot() % 2 == 0
    if push_second:
        queue.push(5)

    output("queue_len", len(queue))
    if push_second:
        queue.pop()
    queue.pop()
    queue.discard_empty()


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Unsupported: priority queue

PriorityQueue currently leaves unsupported internal storage even for a fixed sequence of operations.

import sys
from typing import no_type_check

from guppylang import guppy
from guppylang.std.builtins import output
from guppylang.std.collections.priority_queue import empty_priority_queue


@guppy
@no_type_check
def main() -> None:
    # The heap's data-dependent internal loops cannot be fully unrolled for QIR.
    queue = empty_priority_queue[int, 4]()
    queue.push(30, 3)
    queue.push(10, 1)
    queue.push(20, 2)
    output("priority_queue_len", len(queue))
    first_priority, first_value = queue.pop()
    second_priority, second_value = queue.pop()
    output("priority_queue_first_priority", first_priority)
    output("priority_queue_first_value", first_value)
    output("priority_queue_second_priority", second_priority)
    output("priority_queue_second_value", second_value)
    _, final_value = queue.pop()
    output("priority_queue_final_value", final_value)
    queue.discard_empty()


if __name__ == "__main__":
    sys.stdout.buffer.write(main.compile().to_bytes())

Example error:

Compilation failed: Program may panic: Option.unwrap: value is `Some`. Runtime panics are unsupported on H-Series.