diff --git a/source/compiler/qsc/src/codegen/tests.rs b/source/compiler/qsc/src/codegen/tests.rs index ea1eb22735a..e40aa88b2b4 100644 --- a/source/compiler/qsc/src/codegen/tests.rs +++ b/source/compiler/qsc/src/codegen/tests.rs @@ -7,6 +7,7 @@ mod adaptive_profile; mod adaptive_ri_profile; mod adaptive_rif_profile; mod base_profile; +mod early_return_cleanup; use std::{io::Cursor, rc::Rc, sync::Arc}; @@ -91,6 +92,68 @@ fn compile_source_to_qir_result( ) } +#[test] +fn adaptive_qubit_array_without_loop_compiles() { + let source = + r#"@EntryPoint(Adaptive) operation Main() : Bool { use qs = Qubit[4]; return false; }"#; + let _ = compile_source_to_qir(source, Profile::Adaptive.into()); +} + +#[test] +fn adaptive_loop_with_scalar_qubit_and_early_return_compiles() { + let source = r#"@EntryPoint(Adaptive) operation Main() : Bool { for e in [0] { use q = Qubit(); return false; } return false; }"#; + let _ = compile_source_to_qir(source, Profile::Adaptive.into()); +} + +#[test] +fn adaptive_loop_with_qubit_array_without_early_return_compiles() { + let source = r#"@EntryPoint(Adaptive) operation Main() : Bool { for e in [0] { use qs = Qubit[4]; } return false; }"#; + let _ = compile_source_to_qir(source, Profile::Adaptive.into()); +} + +#[test] +fn adaptive_range_loop_with_qubit_array_and_early_return_compiles() { + let source = r#"@EntryPoint(Adaptive) operation Main() : Bool { for e in 0..0 { use qs = Qubit[4]; return false; } return false; }"#; + let _ = compile_source_to_qir(source, Profile::Adaptive.into()); +} + +#[test] +fn adaptive_loop_with_local_qubit_array_and_early_return_compiles() { + let source = r#" + @EntryPoint(Adaptive) + operation Main() : Bool { + for e0 in [0] { + use qa0 = Qubit[4]; + let ret0 = false; + return ret0; + } + return false; + } + "#; + + let _ = compile_source_to_qir(source, Profile::Adaptive.into()); +} + +// Return unification introduces a loop around the early return. Indexing the +// qubit array in that loop used to panic with "qubit should still be alive". +#[test] +fn adaptive_loop_with_indexed_qubit_array_and_early_return_compiles() { + let source = r#" + @EntryPoint(Adaptive) + operation Main() : Int { + use qs = Qubit[2]; + mutable acc = 0; + for j in 0..1 { + if j == 9 { return 0; } + set acc += MResetZ(qs[j]) == One ? 1 | 0; + } + acc + } + "#; + + let _ = compile_source_to_qir(source, Profile::Adaptive.into()); +} + #[test] fn expanding_polymorphic_recursion_reports_source_errors_on_every_compile_profile() { let sources = [ @@ -4805,6 +4868,49 @@ fn early_return_in_dynamic_branch_reinvoke_original_compiles_parity_target() { ); } +#[test] +fn callable_input_qubit_release_is_not_hidden_by_external_owner() { + let caps = TargetCapabilityFlags::from(Profile::AdaptiveRIF) + | TargetCapabilityFlags::DynamicQubitAllocation; + let source = indoc::indoc! {r#" + namespace Test { + operation Run(q : Qubit) : Unit { + QIR.Runtime.__quantum__rt__qubit_release(q); + let fresh = QIR.Runtime.__quantum__rt__qubit_allocate(); + X(q); + QIR.Runtime.__quantum__rt__qubit_release(fresh); + } + } + "#}; + let (store, pkg, items) = compile_and_locate_items(source, &[("Run", true)], caps); + let external = Rc::new(qsc_eval::val::Qubit(0)); + let args = Value::Qubit((&external).into()); + let (codegen_fir, backend) = + prepare_codegen_fir_from_callable_args(&store, hir_id_for(pkg, items["Run"]), &args, caps) + .unwrap_or_else(|errors| { + panic!("preparation failed: {}", format_interpret_errors(errors)) + }); + let CallableArgsBackend::ReinvokeOriginal { callable, args } = backend else { + panic!("qubit arguments must use reinvocation"); + }; + // Numeric-ID reuse and distinct allocation identities are asserted directly + // by imported_qubit_preserves_identity_across_release_and_reuse. This + // integration test checks the diagnostic through callable QIR generation. + let error = qsc_codegen::qir::fir_to_qir_from_callable( + &codegen_fir.fir_store, + caps, + &codegen_fir.compute_properties, + callable, + args, + ) + .expect_err("a caller-owned Rc must not hide release"); + assert!(matches!( + error, + qsc_partial_eval::Error::EvaluationFailed(message, _) + if message == "qubit used after release" + )); +} + #[test] fn synthetic_path_udt_wrapped_controlled_callable_preserves_functor() { let source = indoc::indoc! {r#" diff --git a/source/compiler/qsc/src/codegen/tests/early_return_cleanup.rs b/source/compiler/qsc/src/codegen/tests/early_return_cleanup.rs new file mode 100644 index 00000000000..4c2ccdd0548 --- /dev/null +++ b/source/compiler/qsc/src/codegen/tests/early_return_cleanup.rs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Compilation-only checks for the straight-line return suffix rule. + +use super::compile_source_to_qir; +use qsc_data_structures::target::Profile; + +fn compile_body(body: &str) -> String { + compile_source_to_qir( + &format!( + r#" + operation Before(q : Qubit) : Unit {{ body intrinsic; }} + operation Operand(q : Qubit) : Unit {{ body intrinsic; }} + operation After(q : Qubit) : Unit {{ body intrinsic; }} + operation Tail(q : Qubit) : Unit {{ body intrinsic; }} + operation Value(q : Qubit) : Bool {{ Operand(q); false }} + operation Run(stop : Bool, q : Qubit) : Bool {{ {body} }} + @EntryPoint(Adaptive) operation Main() : Bool {{ + use q = Qubit(); + Run(MResetZ(q) == One, q) + }} + "# + ), + Profile::Adaptive.into(), + ) +} + +#[test] +fn straight_line_nested_return_keeps_operand_effect_and_drops_dead_effects() { + let qir = compile_body( + "for _ in [0] { Before(q); { { return Value(q); } After(q); } Tail(q); } false", + ); + assert!(qir.contains("call void @Before("), "{qir}"); + assert!(qir.contains("call void @Operand("), "{qir}"); + assert!(!qir.contains("call void @After("), "{qir}"); + assert!(!qir.contains("call void @Tail("), "{qir}"); +} + +#[test] +fn conditional_return_preserves_fallthrough_effects() { + let qir = + compile_body("for _ in [0] { { if stop { return Value(q); } } After(q); } Tail(q); false"); + assert!(qir.contains("call void @Operand("), "{qir}"); + assert!(qir.contains("call void @After("), "{qir}"); + assert!(qir.contains("call void @Tail("), "{qir}"); +} + +#[test] +fn loop_return_preserves_zero_iteration_fallthrough_effects() { + let qir = compile_body("{ while stop { return Value(q); } } After(q); false"); + assert!(qir.contains("call void @Operand("), "{qir}"); + assert!(qir.contains("call void @After("), "{qir}"); +} + +#[test] +fn nested_array_and_scalar_cleanup_keeps_return_operand_live() { + let qir = compile_body( + "use outer = Qubit(); for _ in [0] { use qs = Qubit[2]; { use inner = Qubit(); Before(outer); Before(qs[0]); return Value(inner); } After(q); } Tail(q); false", + ); + assert!(qir.contains("call void @Before("), "{qir}"); + assert!(qir.contains("call void @Operand("), "{qir}"); + assert!(!qir.contains("call void @After("), "{qir}"); + assert!(!qir.contains("call void @Tail("), "{qir}"); +} + +#[test] +fn conditional_array_cleanup_preserves_both_paths() { + let qir = compile_body( + "for _ in [0] { use qs = Qubit[2]; if stop { return Value(qs[0]); } After(qs[1]); } Tail(q); false", + ); + assert!(qir.contains("call void @Operand("), "{qir}"); + assert!(qir.contains("call void @After("), "{qir}"); + assert!(qir.contains("call void @Tail("), "{qir}"); +} + +#[test] +fn conditional_operand_return_compiles_for_adaptive_profiles() { + // Return-bearing conditional operands compile across adaptive target profiles. + let source = r#" + function Run(stop : Bool) : Int { + let v = stop ? 1 + (return 1) | 2; + v + } + @EntryPoint(Adaptive_RIF) operation Main() : Int { Run(true) } + "#; + let _ = compile_source_to_qir(source, Profile::AdaptiveRIF.into()); + let source = r#" + operation Run(stop : Bool, q : Qubit) : Int { + X(q); + let m = MResetZ(q) == One ? 1 | 0; + let v = stop ? 1 + (return 100 + m) | m + 1; + v * 10 + m + } + @EntryPoint(Adaptive_RIF) operation Main() : (Int, Int) { + use q = Qubit(); + (Run(true, q), Run(false, q)) + } + "#; + for profile in [Profile::AdaptiveRI, Profile::AdaptiveRIF, Profile::Adaptive] { + let _ = compile_source_to_qir(source, profile.into()); + } +} diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs b/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs index 8e435a420ad..08e60bb9cfd 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/lower.rs @@ -65,6 +65,23 @@ fn contains_return_in_while_stmt(package: &Package, stmt_id: StmtId) -> bool { } } +// Only follow straight-line blocks: a return inside a loop or one arm of +// a conditional does not establish that the containing statement returns. +fn stmt_unconditionally_returns(package: &Package, stmt_id: StmtId) -> bool { + let (StmtKind::Expr(expr_id) | StmtKind::Semi(expr_id)) = package.get_stmt(stmt_id).kind else { + return false; + }; + match package.get_expr(expr_id).kind { + ExprKind::Return(_) => true, + ExprKind::Block(block_id) => package + .get_block(block_id) + .stmts + .iter() + .any(|&stmt_id| stmt_unconditionally_returns(package, stmt_id)), + _ => false, + } +} + fn sync_block_type_to_stmt_or_unit(package: &mut Package, block_id: BlockId) { let trailing_ty = match package.get_block(block_id).stmts.last() { Some(&stmt_id) => match package.get_stmt(stmt_id).kind { @@ -247,6 +264,10 @@ enum FlagBlockOutput { Unit, } +fn ends_unit_block(package: &Package, stmt_id: StmtId, output: FlagBlockOutput) -> bool { + matches!(output, FlagBlockOutput::Unit) && stmt_unconditionally_returns(package, stmt_id) +} + impl FlagBlockOutput { fn lazy(self) -> Self { match self { @@ -338,11 +359,9 @@ fn transform_block_stmts_with_flags( // - does it hold a `while` loop that itself contains a `return`? // - does it contain a `return` anywhere? // - is it the block's final trailing expression (the block's value)? - let has_return_in_while = match &package.get_stmt(stmt_id).kind { - StmtKind::Expr(e) | StmtKind::Semi(e) => contains_return_in_while_expr(package, *e), - _ => false, - }; + let has_return_in_while = contains_return_in_while_stmt(package, stmt_id); let has_return = contains_return_in_stmt(package, stmt_id); + let ends_unit_block = ends_unit_block(package, stmt_id, output); let is_final_trailing_expr = output.final_trailing_expr_strategy().is_some() && index == original_stmts.len() - 1 && matches!(package.get_stmt(stmt_id).kind, StmtKind::Expr(_)); @@ -451,6 +470,11 @@ fn transform_block_stmts_with_flags( has_return, seen_return_bearing_stmt, ); + // The return has already emitted its cleanup and flag/slot writes. + // Any remaining statements on this straight-line path are unreachable. + if ends_unit_block { + break; + } } new_stmts diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/flag_strategy.rs b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/flag_strategy.rs index 5549c5b9309..a13b180df4c 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/flag_strategy.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/flag_strategy.rs @@ -193,9 +193,6 @@ fn while_body_with_call_arg_return() { __ret_val = 42; __has_returned = true; }; - if (not __has_returned) { - i += 1; - }; } if __has_returned { diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/hoist_expression.rs b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/hoist_expression.rs index c477e7bdc40..ac22138f613 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/hoist_expression.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/normalize/tests/hoist_expression.rs @@ -556,29 +556,6 @@ fn hoist_return_in_range_endpoint() { __ret_val = 5; __has_returned = true; }; - mutable _index_id_31 : Int = if (not __has_returned) { - _range_id_28.Start - } else { - 0 - }; - let _step_id_36 : Int = if (not __has_returned) { - _range_id_28.Step - } else { - 0 - }; - let _end_id_41 : Int = if (not __has_returned) { - _range_id_28.End - } else { - 0 - }; - if (not __has_returned) { - while ((_step_id_36 > 0) and (_index_id_31 <= _end_id_41)) or ((_step_id_36 < 0) and (_index_id_31 >= _end_id_41)) { - let i : Int = _index_id_31; - sum += i; - _index_id_31 += _step_id_36; - } - - }; } if __has_returned { @@ -668,29 +645,6 @@ fn hoist_return_in_local_init_preserves_binding() { __ret_val = 5; __has_returned = true; }; - mutable _index_id_95 : Int = if (not __has_returned) { - _range_id_92.Start - } else { - 0 - }; - let _step_id_100 : Int = if (not __has_returned) { - _range_id_92.Step - } else { - 0 - }; - let _end_id_105 : Int = if (not __has_returned) { - _range_id_92.End - } else { - 0 - }; - if (not __has_returned) { - while ((_step_id_100 > 0) and (_index_id_95 <= _end_id_105)) or ((_step_id_100 < 0) and (_index_id_95 >= _end_id_105)) { - let i : Int = _index_id_95; - sum += i; - _index_id_95 += _step_id_100; - } - - }; } if __has_returned { diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs index 4496ee01048..99563182e0d 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests/flag_strategy.rs @@ -1335,12 +1335,6 @@ fn recursive_while_body_qubit_suffix_uses_lazy_continuation() { __ret_val = 1; __has_returned = true; }; - if (not __has_returned) { - let q : Qubit = __quantum__rt__qubit_allocate(); - Reset(q); - i += 1; - __quantum__rt__qubit_release(q); - }; } if __has_returned { @@ -1390,14 +1384,6 @@ fn recursive_nested_block_qubit_suffix_uses_lazy_continuation() { __ret_val = 1; __has_returned = true; }; - if (not __has_returned) { - let q : Qubit = __quantum__rt__qubit_allocate(); - Reset(q); - __quantum__rt__qubit_release(q); - }; - }; - if (not __has_returned) { - i += 1; }; } @@ -1446,13 +1432,6 @@ fn recursive_qubit_suffix_after_defaultable_local_uses_single_lazy_continuation( __ret_val = 1; __has_returned = true; }; - if (not __has_returned) { - let fallback : Int = i + 1; - let q : Qubit = __quantum__rt__qubit_allocate(); - Reset(q); - i = fallback; - __quantum__rt__qubit_release(q); - }; } if __has_returned { diff --git a/source/compiler/qsc_fir_transforms/src/return_unify/tests/qubit_release.rs b/source/compiler/qsc_fir_transforms/src/return_unify/tests/qubit_release.rs index 66a1b09af34..b3af61fef87 100644 --- a/source/compiler/qsc_fir_transforms/src/return_unify/tests/qubit_release.rs +++ b/source/compiler/qsc_fir_transforms/src/return_unify/tests/qubit_release.rs @@ -390,3 +390,45 @@ fn qubit_arrays_should_be_able_to_be_threaded_through_early_return() { "#]], ); } + +#[test] +fn early_return_preserves_operand_and_scope_cleanup_order() { + let result = compile_no_hoist_return_unified(indoc! {r#" + namespace Test { + operation Before(q : Qubit) : Unit { body intrinsic; } + operation Operand(q : Qubit) : Unit { body intrinsic; } + operation After(q : Qubit) : Unit { body intrinsic; } + operation Value(q : Qubit) : Bool { Operand(q); false } + @EntryPoint() operation Main() : Bool { + use outer = Qubit(); + for _ in [0] { + use qs = Qubit[2]; + { + use inner = Qubit(); + Before(outer); + Before(qs[0]); + return Value(inner); + } + After(outer); + } + false + } + } + "#}); + let mut suffix = result.after.as_str(); + for effect in [ + "Value(inner)", + "__quantum__rt__qubit_release(inner);", + "ReleaseQubitArray(qs);", + "__quantum__rt__qubit_release(outer);", + "__has_returned = true;", + ] { + let (_, rest) = suffix + .split_once(effect) + .unwrap_or_else(|| panic!("missing ordered effect {effect}\n{}", result.after)); + suffix = rest; + } + assert!(!suffix.contains("__quantum__rt__qubit_release(inner);")); + assert!(!suffix.contains("ReleaseQubitArray(qs);")); + assert!(!result.after.contains("After(outer)"), "{}", result.after); +} diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index fca7ce73497..5a54d869a8c 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -86,6 +86,13 @@ pub fn partially_evaluate( } /// Partially evaluates a callable with the specified arguments. +/// +/// The caller must supply live qubit arguments, including qubits captured by +/// closures. Their identities are registered as inputs to this evaluation; +/// releases performed by this evaluation are tracked independently. A +/// `QubitRef` carries no source-manager liveness information beyond its weak +/// reference, so a reference retained by another owner cannot establish that +/// the source manager has not released it. pub fn partially_evaluate_call( package_store: &PackageStore, compute_properties: &PackageStoreComputeProperties, @@ -549,6 +556,19 @@ impl<'a> PartialEvaluator<'a> { } fn invoke(mut self, callable: StoreItemId, args: Value) -> Result { + // Import caller-owned identities once, before evaluating the body. + // Later uses must pass the manager's allocation-aware liveness check; + // an external strong reference must not mask a release in this call. + for qubit in args.qubits() { + if !self.resource_manager.register_input_qubit(&qubit) { + return Err( + EvalError::QubitUsedAfterRelease(map_fir_package_span_to_hir( + fir::PackageSpan::new(callable.package, Default::default()), + )) + .into(), + ); + } + } // Evaluate the callalbe. let ret_val = self.eval_global_call(callable, args)?.into_value(); let global = self @@ -1911,11 +1931,11 @@ impl<'a> PartialEvaluator<'a> { let qubits = args_value.qubits(); let qubits_len = qubits.len(); if qubits_len > 0 { - let qubits = qubits + let live_qubits = qubits .iter() - .filter_map(|q| q.try_deref().map(|q| q.0)) - .collect::>(); - if qubits.len() != qubits_len { + .filter(|q| self.resource_manager.is_qubit_live(q)) + .count(); + if live_qubits != qubits_len { return if callable_decl.name.name.as_ref() == "__quantum__rt__qubit_release" { Err(EvalError::QubitDoubleRelease(args_span).into()) } else { @@ -2599,8 +2619,10 @@ impl<'a> PartialEvaluator<'a> { // Evaluate the body expression. // First, we cache the current static variable mappings so that we can restore them later. let cached_mappings = self.clone_current_static_var_map(); + let qubit_state_before_branch = self.resource_manager.save_qubit_state(); let if_true_block_id = self.eval_expr_if_branch(body_expr_id, continuation_block_node_id, maybe_if_expr_var)?; + let qubit_state_after_true = self.resource_manager.save_qubit_state(); // Evaluate the otherwise expression (if any), and determine the block to branch to if the condition is false. let if_false_block_id = if let Some(otherwise_expr_id) = otherwise_expr_id { @@ -2608,6 +2630,8 @@ impl<'a> PartialEvaluator<'a> { let post_if_true_mappings = self.clone_current_static_var_map(); // Restore the cached mappings from before evaluating the true block. self.overwrite_current_static_var_map(cached_mappings); + self.resource_manager + .restore_qubit_state(qubit_state_before_branch); let if_false_block_id = self.eval_expr_if_branch( otherwise_expr_id, continuation_block_node_id, @@ -2622,10 +2646,20 @@ impl<'a> PartialEvaluator<'a> { // the variable is no longer static across the if expression. self.keep_matching_static_var_mappings(&cached_mappings); + // With no otherwise branch, the false path leaves resources exactly + // as they were before the true branch was evaluated. + self.resource_manager + .restore_qubit_state(qubit_state_before_branch); + // Since there is no otherwise block, we branch to the continuation block. continuation_block_node_id }; + // Preserve the true branch's peak qubit count even when its extra + // allocations have been released and the false path is implicit. + self.resource_manager + .merge_qubit_state(qubit_state_after_true); + // Finally, we insert the branch instruction. let condition_value_var = condition_value.unwrap_var(); let condition_rir_var = map_eval_var_to_rir_var(condition_value_var); diff --git a/source/compiler/qsc_partial_eval/src/management.rs b/source/compiler/qsc_partial_eval/src/management.rs index 728bc96d0da..764518e6dc7 100644 --- a/source/compiler/qsc_partial_eval/src/management.rs +++ b/source/compiler/qsc_partial_eval/src/management.rs @@ -27,7 +27,75 @@ pub struct ResourceManager { next_var: usize, } +#[derive(Clone)] +pub(crate) struct QubitStateSnapshot { + qubits_in_use: Vec, + qubit_id_map: IndexMap, + qubit_tracker: FxHashSet>, +} + impl ResourceManager { + /// Register a caller-supplied qubit before evaluating any statements. + /// This is an input boundary, not a way to revive released allocations. + pub(crate) fn register_input_qubit(&mut self, q: &QubitRef) -> bool { + let Some(q) = q.try_deref() else { + return false; + }; + if self.qubit_id_map.contains_key(q.0) { + return self + .qubit_tracker + .get(&q) + .is_some_and(|active| Rc::ptr_eq(active, &q)); + } + let slot = self.qubits_in_use.len(); + self.qubits_in_use.push(true); + self.qubit_id_map.insert(q.0, slot); + self.qubit_tracker.insert(q); + true + } + + pub(crate) fn save_qubit_state(&self) -> QubitStateSnapshot { + QubitStateSnapshot { + qubits_in_use: self.qubits_in_use.clone(), + qubit_id_map: self.qubit_id_map.clone(), + qubit_tracker: self.qubit_tracker.clone(), + } + } + + pub(crate) fn restore_qubit_state(&mut self, state: QubitStateSnapshot) { + self.qubits_in_use = state.qubits_in_use; + self.qubit_id_map = state.qubit_id_map; + self.qubit_tracker = state.qubit_tracker; + } + + pub(crate) fn merge_qubit_state(&mut self, other: QubitStateSnapshot) { + if self.qubits_in_use.len() < other.qubits_in_use.len() { + self.qubits_in_use.resize(other.qubits_in_use.len(), false); + } + for (slot, other_in_use) in other.qubits_in_use.iter().enumerate() { + self.qubits_in_use[slot] |= other_in_use; + } + + for (id, slot) in other.qubit_id_map.iter() { + if !self.qubit_id_map.contains_key(id) { + self.qubit_id_map.insert(id, *slot); + } + } + self.qubit_tracker.extend(other.qubit_tracker); + } + + pub fn is_qubit_live(&self, q: &QubitRef) -> bool { + q.try_deref().is_some_and(|q| { + // A snapshot can keep a released qubit alive after its numeric ID + // has been reused. Require the allocation currently tracked for it. + self.qubit_id_map.contains_key(q.0) + && self + .qubit_tracker + .get(&q) + .is_some_and(|active| Rc::ptr_eq(active, &q)) + }) + } + pub fn map_qubit(&self, q: &QubitRef) -> usize { let q = q.deref(); *self @@ -192,3 +260,56 @@ impl Backend for QuantumIntrinsicsChecker { } } } + +#[cfg(test)] +mod input_qubit_tests { + use super::*; + + #[test] + fn imported_qubit_preserves_identity_across_release_and_reuse() { + let mut manager = ResourceManager::default(); + let external = Rc::new(Qubit(0)); + let original = QubitRef::from(&external); + assert!(manager.register_input_qubit(&original)); + assert!(manager.register_input_qubit(&original)); + assert!(manager.is_qubit_live(&original)); + let conflicting = Rc::new(Qubit(0)); + assert!(!manager.register_input_qubit(&QubitRef::from(&conflicting))); + let snapshot = manager.save_qubit_state(); + manager.release_qubit(&original); + assert!(!manager.is_qubit_live(&original)); + let replacement = manager.allocate_qubit(); + let replacement_owner = replacement.deref(); + assert_eq!(replacement_owner.0, external.0, "the numeric ID is reused"); + assert!(!Rc::ptr_eq(&replacement_owner, &external)); + assert!(manager.is_qubit_live(&replacement)); + assert!(!manager.is_qubit_live(&original)); + manager.restore_qubit_state(snapshot); + assert!(manager.is_qubit_live(&original)); + assert!(!manager.is_qubit_live(&replacement)); + } + + #[test] + fn imported_expired_qubit_is_rejected() { + let q = Rc::new(Qubit(2)); + let reference = QubitRef::from(&q); + drop(q); + assert!(!ResourceManager::default().register_input_qubit(&reference)); + } + + #[test] + fn imported_sparse_ids_and_new_allocations_use_distinct_slots() { + let mut manager = ResourceManager::default(); + let first = Rc::new(Qubit(42)); + let second = Rc::new(Qubit(0)); + let first_ref = QubitRef::from(&first); + let second_ref = QubitRef::from(&second); + assert!(manager.register_input_qubit(&first_ref)); + assert!(manager.register_input_qubit(&second_ref)); + let fresh = manager.allocate_qubit(); + assert_eq!(manager.map_qubit(&first_ref), 0); + assert_eq!(manager.map_qubit(&second_ref), 1); + assert_eq!(manager.map_qubit(&fresh), 2); + assert!(manager.is_qubit_live(&fresh)); + } +} diff --git a/source/compiler/qsc_partial_eval/src/tests/qubits.rs b/source/compiler/qsc_partial_eval/src/tests/qubits.rs index 8d045b24c89..58f00445046 100644 --- a/source/compiler/qsc_partial_eval/src/tests/qubits.rs +++ b/source/compiler/qsc_partial_eval/src/tests/qubits.rs @@ -491,6 +491,151 @@ fn qubit_double_release_triggers_runtime_error() { ); } +#[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" + )); +} + +#[test] +fn released_qubit_id_reused_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); + let fresh = QIR.Runtime.__quantum__rt__qubit_allocate(); + Op(fresh); + Op(q); + QIR.Runtime.__quantum__rt__qubit_release(fresh); + } + } + } + "#, + }); + + assert!(matches!( + error, + crate::Error::EvaluationFailed(message, _) + if message == "qubit used after release" + )); +} + +#[test] +fn released_qubit_id_reused_in_else_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 { + Op(q); + } else { + QIR.Runtime.__quantum__rt__qubit_release(q); + let fresh = QIR.Runtime.__quantum__rt__qubit_allocate(); + Op(fresh); + Op(q); + QIR.Runtime.__quantum__rt__qubit_release(fresh); + } + } + } + "#, + }); + + assert!(matches!( + error, + crate::Error::EvaluationFailed(message, _) + if message == "qubit used after release" + )); +} + +#[test] +fn fresh_qubit_after_release_inside_dynamic_branch_is_live() { + let program = get_rir_program(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); + let fresh = QIR.Runtime.__quantum__rt__qubit_allocate(); + Op(fresh); + QIR.Runtime.__quantum__rt__qubit_release(fresh); + } + } + } + "#, + }); + + assert_eq!(program.num_qubits, 2); +} + +#[test] +fn dynamic_branch_without_else_preserves_peak_qubit_count() { + let program = get_rir_program(indoc! { + r#" + namespace Test { + operation Op(q : Qubit) : Unit { body intrinsic; } + + @EntryPoint() + operation Main() : Unit { + use control = Qubit(); + + if MResetZ(control) == One { + use qs = Qubit[3]; + Op(qs[0]); + Op(qs[1]); + Op(qs[2]); + } + } + } + "#, + }); + + // The branch-local qubits are released, but its peak slots remain part + // of the statically allocated program even on an implicit false path. + assert_eq!(program.num_qubits, 4); +} + #[test] fn qubit_relabel_in_dynamic_block_triggers_capability_error() { let error = get_partial_evaluation_error(indoc! {