Skip to content

[Panic] Adaptive: an early return inside an emitted loop leaks a qubit release past the loop #4058

Open

Description

On Adaptive, an early return inside a for loop over a range, with a qubit array in scope,
panics with "qubit should still be alive". Adaptive_RI and Adaptive_RIF compile both programs
below to the right result.

How this differs from #3940. #3940 has the return under an if inside the loop; the
qubit releases on the returning if arm leak into the other arm because partial evaluation
did not restore qubit state at the if join. #3943 fixes that by saving and merging qubit
state at if joins, and #3940's programs compile with it. The two programs here are not fixed
by #3943 (checked on its head abe8816), nor by #4032 or #4033:

The emitted-loop case probably also shows up as a refusal of a valid program: with an empty range
(for i in 0..-1..0 { return MResetZ(qs[2]); }) and a later MResetZ(qs[1]), Adaptive
reports "qubit used after release".

Reproduces on main (94b1ef2), #3943 (abe8816), #4032 (99464f3, 8de8fa2) and #4033
(64acdf2); Adaptive only.

"""
Adaptive: an early `return` in a `for` loop over a range, with a qubit array
in scope, panics with "qubit should still be alive" (G1), and a variant with a
later taken `return` and a new allocation still panics with #3943 applied (G3).

Exits 1 while either panic reproduces on Adaptive, and 0 once it no longer does.
"""

import sys

from qdk import qsharp

PROGRAMS = {
    # A `return` directly in the loop body.
    "return_in_loop_body": """
operation Main() : Int {
    use qs = Qubit[2];
    for i in 0..1 {
        return i;
    }
    5
}
""",
    # The #3940 shape (return under an `if` in the loop), then a taken return
    # and a new allocation.
    "taken_return_then_alloc": """
operation Main() : Unit {
    use qs = Qubit[2];
    for i in 0..1 {
        if i < 0 { return (); }
    }
    if true { return (); }
    use more = Qubit[3];
}
""",
}

reproduced = False
for name, source in PROGRAMS.items():
    qsharp.init(target_profile=qsharp.TargetProfile.Unrestricted)
    qsharp.eval(source)
    print(f"{name}: simulator {qsharp.eval('Main()')}")
    for profile in ["Adaptive_RI", "Adaptive_RIF", "Adaptive"]:
        qsharp.init(target_profile=getattr(qsharp.TargetProfile, profile))
        qsharp.eval(source)
        try:
            qsharp.compile("Main()")
            print(f"  {profile:13} compiles")
        except BaseException as e:  # a Rust panic surfaces as PanicException
            panicked = type(e).__name__ == "PanicException"
            reproduced |= panicked
            print(f"  {profile:13} {type(e).__name__}: {str(e).splitlines()[0]}")

print("REPRODUCED" if reproduced else "NOT REPRODUCED")
sys.exit(1 if reproduced else 0)

Output with #3943 applied:

return_in_loop_body: simulator 0
  Adaptive_RI   compiles
  Adaptive_RIF  compiles
  Adaptive      PanicException: qubit should still be alive
taken_return_then_alloc: simulator None
  Adaptive_RI   compiles
  Adaptive_RIF  compiles
  Adaptive      PanicException: IndexMap::index: index out of bounds
REPRODUCED

On main, taken_return_then_alloc panics with "qubit should still be alive" instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions