Repository navigation
Fix qubit state across dynamic branches - #3943
Demetrios Chiuratto Agourakis (agourakis82) wants to merge 11 commits into
Conversation
Signed-off-by: Demetrios Agourakis <[email protected]>
|
@microsoft-github-policy-service agree |
|
The workflows for this PR are awaiting maintainer approval. Could you approve the pending runs so the change can receive build and test validation? |
|
Demetrios Chiuratto Agourakis (@agourakis82) Thanks for this! We're pausing a lot of compiler changes until this monster PR stack goes in: #3961. Thanks for your patience! |
Integrate upstream main after the microsoft#3961 stack landed. Resolve the codegen test conflict by preserving the new upstream tests and the microsoft#3940 indexed-qubit-array early-return regression. Validation against main 3977495: the regression fails without the production fix and passes with it; 221 codegen tests pass (one ignored) and all 425 partial-evaluation tests pass. Formatting checks pass.
|
Andrew Casey (@amcasey) Thanks for the update. #3961 is now merged, and this branch includes the subsequent main update at The current CI Build and Test run is awaiting approval ( Prepared with Codex assistance. |
|
No — I checked your exact variant on With the same native Q#-to-QIR test harness, the original indexed-array reproducer passes on the PR while your variant fails. Against base The variant's backtrace reaches Investigation and reply prepared with Codex assistance. |
| pub(crate) struct QubitState { | ||
| qubits_in_use: Vec<bool>, | ||
| qubit_id_map: IndexMap<usize, usize>, | ||
| qubit_tracker: FxHashSet<Rc<Qubit>>, |
There was a problem hiding this comment.
Is this going to keep Qubits alive longer than they used to be? I think there's behavior associated with their deallocation.
There was a problem hiding this comment.
Via copilot
#[test]
fn released_qubit_inside_dynamic_branch_reports_error() {
let error = get_partial_evaluation_error(indoc! {
r#"
namespace Test {
operation Op(q : Qubit) : Unit { body intrinsic; }
@EntryPoint()
operation Main() : Unit {
use control = Qubit();
let q = QIR.Runtime.__quantum__rt__qubit_allocate();
if MResetZ(control) == One {
QIR.Runtime.__quantum__rt__qubit_release(q);
Op(q);
}
}
}
"#,
});
assert!(matches!(
error,
crate::Error::EvaluationFailed(message, _)
if message == "qubit used after release"
));
}There was a problem hiding this comment.
The snapshot does extend the Rc payload lifetime while the alternate path is evaluated. Qubit has no custom Drop implementation; logical resource release is explicit in free_qubit_id. A Weak reference can therefore still upgrade after logical release.
The map-only guard needed one more condition when an ID is recycled. In 015eb55, the guard requires both an active ID and Rc::ptr_eq with the allocation in qubit_tracker. On 2c666a4, regressions that release q, allocate fresh with the recycled ID, and then use q are accepted in both the true and else branches. With this change they report qubit used after release, while the fresh-allocation control remains valid.
Native Q#-to-IR checks passed: 17 qubit tests, 31 branching tests, and six #3940 cases including the original reproducer, your loop-local variant and four controls. The source hashes match the isolated compiler build. This validates these liveness cases; the snapshot still retains strong Rc ownership.
Does this address the lifetime concern, or is there another deallocation-dependent path you would like covered?
Investigation, patch and reply prepared with Codex assistance.
There was a problem hiding this comment.
Merged main 0eb69e16 in 9ecb0a7. Snapshots and restores now carry physical capacity, both free heaps, logical-to-physical mappings and allocation identities. The callable QIR test also supplies the new LanguageFeatures argument.
Review found a compatibility issue in the initial integration candidate. Two mutually exclusive branches can explicitly allocate distinct Rc identities with the same logical ID. The published head compiles that input in Adaptive, while the initial candidate's new join assertion panicked. The merge restores the published overlap selection and adds both a component capacity regression and a full Adaptive QIR regression. The strict allocation-identity check at use time is preserved.
Native compiler-only validation with Rust 1.96 passed all 491 partial-evaluation tests, 12 allocator lifecycle controls, 322 selected structural return-unification tests, 254 code-generator tests and 133 RIR tests. The six cleanup cases, 85 Adaptive codegen cases and 26 base-profile cases also pass, with one existing base-profile test ignored. Formatting and Clippy for the affected compiler packages pass with -D warnings. The macOS deep-recursion test uses RUST_MIN_STACK=16777216.
I also reproduced an existing liveness gap on the exact previously published head 6c50b68d. After partial evaluation, this pattern can still be accepted despite the one-sided release.
let q = QIR.Runtime.__quantum__rt__qubit_allocate();
if MResetZ(control) == One {
QIR.Runtime.__quantum__rt__qubit_release(q);
}
Op(q);That behavior predates this integration. A separate hardening experiment detected it but rejected valid guarded cleanup across a loop exit, so the continuation/path-correlation work remains an open design follow-up. Is separating the allocator compatibility update from that semantic follow-up an acceptable boundary for this PR?
Prepared with OpenAI Codex assistance, including independent source review.
There was a problem hiding this comment.
Implemented the liveness follow-up in 074da93.
Snapshots and release-history entries retain Rc<Qubit> payloads for allocation-identity comparisons. Logical release is now recorded separately from physical-slot reservation. For statically tracked allocations, a later use or release checks the exact allocation identity against every resource path reaching that operation. A surviving snapshot or recycled numeric ID therefore cannot revive a released allocation.
The resource analysis follows Boolean binding identities and assignment/copy semantics, including the return guards produced by lowering. It filters resource paths at ordinary branches and short-circuit edges while preserving the emitted runtime guards. Caller-visible release effects are joined when the callee completes. Callables with transitive release effects use the existing inline path for static qubit management, so their actual argument identities remain available to that analysis.
The loop check records use/release events on the emitted CFG and checks entry allocations across reachable backedges, without evaluating the source effects again. The loop exit also includes the zero-iteration state. Loop-entry alternatives retain their release and Boolean facts, including identities that are live on only some incoming paths. Delayed physical reuse creates a fresh allocation identity while preserving the slot and capacity accounting.
The three previously demonstrated invalid programs now produce the required diagnostics: one-sided release followed by unconditional use, use after release on a subsequent iteration, and double release on a subsequent iteration. The valid cleanup continuations still compile, including both partially returning arms and an outer array whose loop body always returns but whose zero-iteration path continues.
Native compiler-only validation on the frozen candidate passed all 507 partial-evaluation tests, 26 cleanup/liveness QIR tests, 51 Adaptive profile tests, 15 Adaptive RI tests, 13 Adaptive RIF tests, 26 base-profile tests (one existing test ignored), 254 code-generator tests and 133 RIR tests. Strict Clippy for the affected compiler libraries and tests, formatting and diff checks also passed. An independent rebuild in a separate target also passed all 507 partial-evaluation tests and all 26 cleanup/liveness QIR tests. The six source hashes were verified before and after that replay.
One existing allocator test had equated an occupied slot with unconditional liveness after one branch released its input. Its two liveness assertions now require that identity to be invalid, while retaining the slot, capacity and numeric-ID checks. The selected profile tests passed against their checked-in expectations.
Prepared and validated with OpenAI Codex assistance, including separate implementation, source-review and compiler-test lanes.
The pushed head is awaiting upstream workflow results and maintainer re-review.
Signed-off-by: Demetrios Agourakis <[email protected]>
Stop flag lowering of unit-valued blocks after a straight-line statement that unconditionally returns. This avoids evaluating duplicate array cleanup in the loop-local qubit variant of microsoft#3940. Preserve conditional and loop-containing suffixes unless a separate unconditional return establishes termination. Add native QIR regressions and update six snapshots to omit unreachable statements. Signed-off-by: Demetrios Agourakis <[email protected]>
|
The variant is now covered in It exposed an unreachable cleanup suffix left after an unconditional return in the loop body. Partial evaluation visited that suffix and released the same array twice. The follow-up removes such suffixes from unit-valued blocks, following only straight-line nested blocks. It preserves qubit lifetime checks and the changes in On Prepared with Codex assistance. |
|
Andrew Casey (@amcasey) Thanks for the careful review. I’ve pushed two focused follow-ups to address the comments and the loop-local variant of #3940. In
The regression test Validation on the current branch head (
Please take another look when convenient. |
|
Demetrios Chiuratto Agourakis (@agourakis82) Thanks for the updates! Just a heads up - I'll be out for a few days. Hopefully, someone will be able to pick it up while I'm gone, but I can't promise anything. |
Require the active tracker allocation as well as its numeric ID when checking liveness across branch snapshots. Cover stale references in both branches, valid ID reuse, and the implicit-false-path peak count. Keep the resource-state join outside otherwise to preserve that peak. Co-Authored-By: Codex <[email protected]>
|
Demetrios Chiuratto Agourakis (@agourakis82) Both CI failures are |
|
Thanks for flagging this. I've checked the logs: both failing tests exercise early returns in dynamic branches and report qubit used after release. I'll trace where the qubit state diverges in those cases before changing the implementation. There is also a separate Clippy failure in return_unify/lower.rs requesting let … else. |
Document the live-input contract for caller-supplied qubits and keep input registration separate from subsequent allocation-aware liveness checks. Add codegen coverage that retains an external owner across release and numeric-ID reuse.
|
I've pushed the fix and integrated current main. Both failures came from live qubits supplied through closure captures: the new partial evaluator had not registered those inputs before checking their liveness. Input registration now happens before evaluating the body. The stricter checks remain in place afterward, and a new regression confirms that an external strong reference cannot hide a release followed by numeric-ID reuse. Both reported tests pass locally, along with 541 qsc tests, 439 qsc_partial_eval tests, 471 return_unify tests, and Clippy for the three affected packages. The new CI run is awaiting approval. |
I'm probably missing something obvious, but I only see tests and comments in your last commit. |
|
You're right — Specifically, |
Andrew Casey (amcasey)
left a comment
There was a problem hiding this comment.
I'm a bit nervous about the new return handling. I'm pretty sure there's a reason we weren't already doing that. I'll try to confirm
|
I traced the suffix rule through allocation lowering and published seven additional compiler tests in 6c50b68 for operand effects, nested scalar/array cleanup, conditional fallthrough, and a possibly zero-iteration loop. Production code and the original early-return regressions are unchanged by this follow-up. Allocation lowering captures the operand and releases active scopes before the Return. The suffix rule removes the later duplicate suffix only in Unit blocks, following direct returns and straight-line blocks. The structural FIR test checks Value(inner) before inner/array/outer cleanup and the return flag. Disabling only that trimming rule reproduces qubit should still be alive in the existing loop-local array regression; restoring it passes. These checks do not prove general correctness or settle the return-handling and lifetime concerns. All seven new tests, scoped Clippy and formatting pass locally; hosted CI remains a separate approval gate. Investigation and additional tests prepared with OpenAI Codex assistance. |
|
Andrew Casey (@amcasey) Could you re-review head The latest follow-up adds targeted return/cleanup tests; the implementation and the earlier release/reuse regressions remain in the preceding commits. The source analysis and validation limits are recorded above. Your concern about the return transformation and qubit lifetime remains open for review; the additional tests are not a claim of general correctness. Prepared with OpenAI Codex assistance. |
|
Demetrios Chiuratto Agourakis (@agourakis82) We haven't forgotten about you! We're just trying to get somethings straightened out before our next release (some of which overlap with this change). So that it doesn't come as a surprise later, I'll mention that while the first part of your change (state snapshots) seemed pretty straightforward, the early return change makes me much more nervous since it's a change to our historical approach (possibly an improvement, but nevertheless a change), so we're going to have to think about it in the context of a larger change (which almost certainly won't meet the bar for the current release). Thanks for your patience! |
Carry capacity and both free heaps through snapshots while retaining the published overlap selection for mutually exclusive branch allocations. Update the caller-qubit codegen test for the LanguageFeatures argument. Assisted-by: OpenAI Codex
Separate allocation validity from physical slot reservation and retain release facts by allocation identity. Track Boolean facts through guards, copies and writes so return cleanup paths stay distinct from continuation. Validate loop-entry identities over the emitted CFG, including repeatable backedges and zero-iteration exits. Evaluate transitive release effects with concrete arguments during static qubit management, and give delayed slot reuse a fresh allocation identity. Add diagnostic and valid-continuation controls for branches, loops, caller-owned qubits, arrays, Boolean updates and delayed release. Assisted-by: OpenAI Codex
Summary
Addresses the original reproducer and the loop-local array variant in #3940.
Partial evaluation was carrying qubit releases from one dynamic branch into the alternate path. The patch snapshots qubit resource state before the branch, restores it for the alternate path, and merges the live resources afterward. Liveness checks consult the active resource map rather than relying on references retained by snapshots.
The follow-up variant also exposed an unreachable cleanup suffix after an unconditional return in a loop. Return unification now stops rewriting a unit-valued block after a statement that unconditionally returns. Detection follows only straight-line nested blocks; a return inside a conditional arm or nested loop does not establish that the enclosing statement returns.
The regression tests cover the original reproducer, the exact variant, a range-loop variant, and controls without an early return, without a loop, and with a single qubit. Six existing snapshots are updated only to remove unreachable statements.
Validation of
2c666a4Native Linux cluster checks with Rust 1.95.0:
cargo test --locked -p qsc --lib issue_3940_ -- --test-threads=4: 6 passed.cargo test --locked -p qsc_fir_transforms --lib return_unify -- --test-threads=4: 463 passed.cargo test --locked -p qsc_partial_eval --lib tests::qubits -- --test-threads=4: 13 passed, including the dynamic-branch use-after-release regression.git diff --checkpassed.With the same regression tests on preceding head
876554c, the maintainer variant and range-loop variant panic; the other four pass. With this follow-up, all six pass. These are focused compiler checks, not a claim that the full workspace suite or GitHub CI has passed on this head.Prepared with Codex assistance. The return-lowering candidate also received a separate coder-next review; executable validation and final review remained part of the contribution workflow.
Additional return/cleanup regression coverage
Commit 6c50b68 adds six Q#-to-QIR compilation tests and one structural FIR test for return-operand effects, nested scalar/array cleanup, conditional fallthrough and possibly zero-iteration loops. Production code and the original early-return regressions remain unchanged by this follow-up.
All seven new tests pass. Scoped Clippy with warnings denied, formatting and whitespace checks pass. The existing early-return test filters also pass (16 qsc and 19 FIR-transform tests); these local checks are separate from hosted CI. The structural test checks operand evaluation before inner/array/outer cleanup and the return flag, without claiming a general semantic proof.
The maintainer's return-handling and snapshot-lifetime questions remain open for review. The investigation and additional compiler tests were prepared with OpenAI Codex assistance.