Repository navigation
Fix qubit state across dynamic branches - #3943
Demetrios Chiuratto Agourakis (agourakis82) wants to merge 9 commits into
Conversation
Signed-off-by: Demetrios Agourakis <demetrios@agourakis.med.br>
|
@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.
Signed-off-by: Demetrios Agourakis <demetrios@agourakis.med.br>
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 <demetrios@agourakis.med.br>
|
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 <noreply@openai.com>
|
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
| } | ||
|
|
||
| #[test] | ||
| fn issue_3940_control_array_without_loop() { |
There was a problem hiding this comment.
Nit: we don't usually include issue numbers in test names. The test should make sense outside the context of a particular bug.
There was a problem hiding this comment.
Incidentally, what does this test? I guess it used to panic?
There was a problem hiding this comment.
Renamed all six tests in abe8816 to describe the behavior without an issue number.
The array-without-loop test you commented on is a compilation control, not the former panic reproducer. The regression is now named adaptive_loop_with_indexed_qubit_array_and_early_return_compiles: it exercises indexed qubit-array access in an Adaptive loop with an early return, which previously panicked with qubit should still be alive. I added that explanation next to the test. The controls distinguish array indexing, loop shape, and early-return handling.
Validation: all 73 codegen::tests::adaptive_ tests pass, including these six; formatting passes.
Prepared with Codex assistance.
| callable, | ||
| args, | ||
| ) | ||
| .expect_err("a caller-owned Rc must not hide release and numeric-ID reuse"); |
There was a problem hiding this comment.
This isn't obviously testing numeric ID reuse
There was a problem hiding this comment.
Agreed: the integration test did not directly establish that the numeric ID was reused. In abe8816, imported_qubit_preserves_identity_across_release_and_reuse now explicitly asserts that the replacement has the original numeric ID and that Rc::ptr_eq is false for the two allocations. It then checks that the replacement is live and the released original is not, followed by the snapshot-restoration checks.
As a discriminating check, I temporarily reduced is_qubit_live to the active-ID-map check alone. The test passed the new ID/identity assertions and then failed at !manager.is_qubit_live(&original) after reallocation. Restoring the allocation-identity check makes all three input-qubit tests pass.
The callable integration test still passes and now describes its narrower role: checking the use-after-release diagnostic through QIR generation. Its comment points to the direct reuse assertions in the resource-manager test.
Prepared with Codex assistance.
|
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! |
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.