Debugging Runtime Errors¶
Importance of Detecting and Catching Runtime Errors¶
Runtime errors are critical issues that occur during program execution and can lead to incorrect results, program crashes, or undefined behavior. Selene provides a local facility to catch runtime errors prior to cloud submission. The function, postprocess_unparsed_stream, post-processes a stream of unparsed shots. It returns the results, in addition to exceptions that occurred during selene processing.
In quantum programming, runtime errors can manifest as:
Array indexing errors: Accessing qubits outside the allocated range
Resource constraints: Exceeding hardware limitations or qubit allocations
Logic errors: Incorrect circuit construction or gate application sequences
Array Indexing Errors¶
Index Out of Bounds Error¶
An out of bounds error occurs when the user program attempts to access an array element at an index that exceeds the valid range of the array. In Selene, arrays have fixed sizes determined at initialization, and accessing indices outside this range [0, size-1] triggers a runtime exception.
Off-by-one errors: Loop conditions that iterate one index too far (e.g.,
range(n+1)instead ofrange(n))Incorrect array size: Allocating an array that is too small for the operations you need to perform
Dynamic index calculations: Computing indices that don’t account for array boundaries
Edge case handling: Special cases at array boundaries that aren’t properly validated
In the code below, an array of 16 qubits is created (valid indices 0-15), but the loop attempts to access index 16 when n = 15 (since j = n + 1). The post-process function, postprocess_unparsed_stream, is used with the kwarg, parse_results=False, set to False.
from guppylang import guppy
from guppylang.std.builtins import array, result, comptime
from guppylang.std.quantum import cx, qubit, measure_array
from guppylang.std.debug import state_result
N_QUBITS = 16
@guppy
def main() -> None:
q = array(qubit() for _ in range(comptime(N_QUBITS)))
for n in range(comptime(N_QUBITS)):
i = n
j = n + 1
cx(q[i], q[j])
c = measure_array(q)
result("measurement_result", c)
hugr = main.compile()
from selene_sim import Stim, build
from selene_sim.result_handling.parse_shot import postprocess_unparsed_stream
from hugr.qsystem.result import QsysResult
hugr_binary = main.compile()
runner = build(hugr_binary)
shots, error = postprocess_unparsed_stream(
runner.run_shots(
simulator=Stim(),
n_qubits=N_QUBITS,
n_shots=10,
parse_results=False
),
)
print(error)
Panic (#1002): Index out of bounds
Adjust loop bounds: Ensure your loop doesn’t exceed the array size
Validate index calculations: Always check that computed indices fall within valid bounds before using them
Add bounds checking: If using dynamic indices, implement explicit validation
Use proper array sizing: Ensure your array is large enough for all anticipated operations
A corrected problem with valid array indexing is shown below.
from guppylang import guppy
from guppylang.std.builtins import array, result
from guppylang.std.quantum import cx, qubit, measure_array
@guppy
def main() -> None:
q = array(qubit() for _ in range(comptime(N_QUBITS)))
for n in range(comptime(N_QUBITS - 1)):
i = n
j = n + 1
cx(q[i], q[j])
c = measure_array(q)
result("measurement_result", c)
hugr = main.compile()
from selene_sim import Stim, build
from selene_sim.result_handling.parse_shot import postprocess_unparsed_stream
from hugr.qsystem.result import QsysResult
hugr_binary = main.compile()
runner = build(hugr_binary)
shots, error = postprocess_unparsed_stream(
runner.run_shots(
simulator=Stim(),
n_qubits=N_QUBITS,
n_shots=10,
parse_results=False
),
)
print(error)
None
Runtime No-Cloning Violation¶
The quantum no-cloning theorem states that it is impossible to create an independent, identical copy of an arbitrary unknown quantum state. Formally, there exists no unitary operator \(U\) such that:
In Guppy, this constraint is generally enforced at compile time. However, the Guppy compiler will not capture no-cloning violations arising from array indexing errors. A no-cloning violation is raised at runtime when a quantum gate receives two references that resolve to the same underlying qubit — a condition known as index aliasing.
Index aliasing arises from array indexing errors in which distinct index variables are inadvertently assigned the same value. Consider a gate \(G(q_i, q_j)\) requiring two distinct qubits: if \(i = j\), the gate operates on a single qubit in both operand positions, which constitutes an illegal state duplication.
In the following example, the assignments i = 2*n and j = 2*n are identical for all \(n\), yielding \(q[i] = q[j]\).
from guppylang import guppy
from guppylang.std.builtins import array, result, comptime
from guppylang.std.quantum import cx, qubit, measure_array
from selene_sim import Stim, build
from selene_sim.result_handling.parse_shot import postprocess_unparsed_stream
N_QUBITS = 16
@guppy
def main() -> None:
q = array(qubit() for _ in range(comptime(N_QUBITS)))
for n in range(comptime(N_QUBITS // 2)):
i = 2 * n
j = 2 * n
cx(q[i], q[j])
c = measure_array(q)
result("measurement_result", c)
runner = build(main.compile(), "no-cloning-error")
shots, error = postprocess_unparsed_stream(
runner.run_shots(
simulator=Stim(),
n_qubits=N_QUBITS,
n_shots=10,
parse_results=False
),
)
print(error)
Panic (#1002): Array element is already borrowed
This can be resolved by setting i = 2 * n and j = 2 * n + 1, yielding \(q[i] \neq q[j]\).
N_QUBITS = 16
@guppy
def main() -> None:
q = array(qubit() for _ in range(comptime(N_QUBITS)))
for n in range(comptime(N_QUBITS // 2)):
i = 2 * n
j = 2 * n + 1
cx(q[i], q[j])
c = measure_array(q)
result("measurement_result", c)
runner = build(main.compile(), "no-cloning-error")
shots, error = postprocess_unparsed_stream(
runner.run_shots(
simulator=Stim(),
n_qubits=N_QUBITS,
n_shots=10,
parse_results=False
),
)
print(error)
None
Debug Termination Conditions¶
Panic¶
Local selene simulation can be used to verify the behaviour of panic() and exit(). The panic function terminates the entire execution of the program, and is used as an exit strategy for abnormalities impacting the entire execution lifecycle (historic and future shots). The program stops at the second shot.
from guppylang.decorator import guppy
from guppylang.std.quantum import qubit, h, measure
from guppylang.std.builtins import result, panic
@guppy
def main() -> None:
q = qubit()
h(q)
outcome = measure(q)
if outcome:
panic("Postselection failed")
result("c", outcome)
runner = build(main.compile(), "panic")
shots, error = postprocess_unparsed_stream(
runner.run_shots(
simulator=Stim(),
n_qubits=1,
n_shots=10,
parse_results=False
),
)
print(error)
print(shots)
Panic (#1001): Postselection failed
[[('EXIT:INT:Postselection failed', 1001)]]
Exit¶
The exit function quits the current shot, but continues with program execution by starting the next shot. The program defined above is rerun with exit instead of panic, leading to completion of all 10 shots, albeit with shots and errors collated and returned togther.
from guppylang.decorator import guppy
from guppylang.std.quantum import qubit, h, measure
from guppylang.std.builtins import result, panic
@guppy
def main() -> None:
q = qubit()
h(q)
outcome = measure(q)
if outcome:
exit("Postselection failed")
result("c", outcome)
runner = build(main.compile(), "panic")
shots, error = postprocess_unparsed_stream(
runner.run_shots(
simulator=Stim(),
n_qubits=1,
n_shots=10,
parse_results=False
),
)
print(error)
print(shots)
None
[[('EXIT:INT:Postselection failed', 1)], [('USER:BOOL:c', 0)], [('USER:BOOL:c', 0)], [('USER:BOOL:c', 0)], [('USER:BOOL:c', 0)], [('EXIT:INT:Postselection failed', 1)], [('EXIT:INT:Postselection failed', 1)], [('EXIT:INT:Postselection failed', 1)], [('EXIT:INT:Postselection failed', 1)], [('EXIT:INT:Postselection failed', 1)]]
Timeout Errors¶
An unbounded loop is a control-flow construct whose termination condition is absent, unreachable, or not guaranteed to be satisfied during execution. As a result, the program fails to reach a halting state within finite time. In Selene simulation, unbounded loops are typically observed as runtime non-termination and are surfaced through a timeout condition. This commonly occurs when a construct such as while True has no reachable exit path. Formally, if loop execution induces a state sequence \(s_0, s_1, s_2, \ldots\) without reaching any terminating state \(s_T\), the computation diverges. Practical detection is therefore bounded by a configured execution-time limit.
The example below demonstrates this behavior: the loop continues indefinitely until the simulator aborts execution after the timeout threshold is exceeded.
from guppylang import guppy
from guppylang.std.quantum import qubit, h, measure
from guppylang.std.builtins import result
import datetime
@guppy
def main() -> None:
while True:
q0: qubit = qubit()
h(q0)
c = measure(q0)
result("r", c)
if c:
break
runner = build(main.compile(), "timeout")
shots, error = postprocess_unparsed_stream(
runner.run_shots(
simulator=Stim(),
n_qubits=1,
n_shots=10,
parse_results=False,
timeout=datetime.timedelta(seconds=1)
),
)
print(error)
print(shots)
None
[[('USER:BOOL:r', 1)], [('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 1)], [('USER:BOOL:r', 0), ('USER:BOOL:r', 1)], [('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 0), ('USER:BOOL:r', 1)], [('USER:BOOL:r', 1)], [('USER:BOOL:r', 1)], [('USER:BOOL:r', 1)], [('USER:BOOL:r', 1)], [('USER:BOOL:r', 1)], [('USER:BOOL:r', 1)]]