diff --git a/compiler/qsc/src/codegen/tests.rs b/compiler/qsc/src/codegen/tests.rs index 4dc3a85317..cfffa26f0b 100644 --- a/compiler/qsc/src/codegen/tests.rs +++ b/compiler/qsc/src/codegen/tests.rs @@ -250,6 +250,137 @@ mod base_profile { !3 = !{i32 1, !"dynamic_result_management", i1 false} "#]].assert_eq(&qir); } + + #[test] + fn qubit_id_swap_results_in_different_id_usage() { + let source = "namespace Test { + @EntryPoint() + operation Main() : (Result, Result) { + use (q0, q1) = (Qubit(), Qubit()); + X(q0); + SwapLabels(q0, q1); + X(q1); + (MResetZ(q0), MResetZ(q1)) + } + }"; + let sources = SourceMap::new([("test.qs".into(), source.into())], None); + let language_features = LanguageFeatures::default(); + let capabilities = TargetCapabilityFlags::empty(); + + let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities); + let qir = get_qir( + sources, + language_features, + capabilities, + store, + &[(std_id, None)], + ) + .expect("Failed to generate QIR"); + expect![[r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__rt__tuple_record_output(i64 2, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__x__body(%Qubit*) + + declare void @__quantum__rt__tuple_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="2" "required_num_results"="2" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3} + + !0 = !{i32 1, !"qir_major_version", i32 1} + !1 = !{i32 7, !"qir_minor_version", i32 0} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + "#]].assert_eq(&qir); + } + + #[test] + fn qubit_id_swap_across_reset_uses_updated_ids() { + let source = "namespace Test { + @EntryPoint() + operation Main() : (Result, Result) { + { + use (q0, q1) = (Qubit(), Qubit()); + X(q0); + SwapLabels(q0, q1); + X(q1); + Reset(q0); + Reset(q1); + } + use (q0, q1) = (Qubit(), Qubit()); + (MResetZ(q0), MResetZ(q1)) + } + }"; + let sources = SourceMap::new([("test.qs".into(), source.into())], None); + let language_features = LanguageFeatures::default(); + let capabilities = TargetCapabilityFlags::empty(); + + let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities); + let qir = get_qir( + sources, + language_features, + capabilities, + store, + &[(std_id, None)], + ) + .expect("Failed to generate QIR"); + expect![[r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__rt__tuple_record_output(i64 2, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__x__body(%Qubit*) + + declare void @__quantum__rt__tuple_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="4" "required_num_results"="2" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3} + + !0 = !{i32 1, !"qir_major_version", i32 1} + !1 = !{i32 7, !"qir_minor_version", i32 0} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + "#]].assert_eq(&qir); + } } mod adaptive_profile { @@ -686,6 +817,239 @@ mod adaptive_ri_profile { "#]].assert_eq(&qir); } + #[test] + fn qubit_id_swap_results_in_different_id_usage() { + let source = "namespace Test { + @EntryPoint() + operation Main() : (Result, Result) { + use (q0, q1) = (Qubit(), Qubit()); + X(q0); + SwapLabels(q0, q1); + X(q1); + (MResetZ(q0), MResetZ(q1)) + } + }"; + let sources = SourceMap::new([("test.qs".into(), source.into())], None); + let language_features = LanguageFeatures::default(); + let capabilities = TargetCapabilityFlags::Adaptive + | TargetCapabilityFlags::QubitReset + | TargetCapabilityFlags::IntegerComputations; + + let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities); + let qir = get_qir( + sources, + language_features, + capabilities, + store, + &[(std_id, None)], + ) + .expect("Failed to generate QIR"); + expect![[r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__rt__tuple_record_output(i64 2, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__x__body(%Qubit*) + + declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*) #1 + + declare void @__quantum__rt__tuple_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="2" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10} + + !0 = !{i32 1, !"qir_major_version", i32 1} + !1 = !{i32 7, !"qir_minor_version", i32 0} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 1, !"classical_ints", i1 true} + !5 = !{i32 1, !"qubit_resetting", i1 true} + !6 = !{i32 1, !"classical_floats", i1 false} + !7 = !{i32 1, !"backwards_branching", i1 false} + !8 = !{i32 1, !"classical_fixed_points", i1 false} + !9 = !{i32 1, !"user_functions", i1 false} + !10 = !{i32 1, !"multiple_target_branching", i1 false} + "#]].assert_eq(&qir); + } + + #[test] + fn qubit_id_swap_across_reset_uses_updated_ids() { + let source = "namespace Test { + @EntryPoint() + operation Main() : (Result, Result) { + { + use (q0, q1) = (Qubit(), Qubit()); + X(q0); + SwapLabels(q0, q1); + X(q1); + Reset(q0); + Reset(q1); + } + use (q0, q1) = (Qubit(), Qubit()); + (MResetZ(q0), MResetZ(q1)) + } + }"; + let sources = SourceMap::new([("test.qs".into(), source.into())], None); + let language_features = LanguageFeatures::default(); + let capabilities = TargetCapabilityFlags::Adaptive + | TargetCapabilityFlags::QubitReset + | TargetCapabilityFlags::IntegerComputations; + + let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities); + let qir = get_qir( + sources, + language_features, + capabilities, + store, + &[(std_id, None)], + ) + .expect("Failed to generate QIR"); + expect![[r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__reset__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__reset__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__rt__tuple_record_output(i64 2, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__x__body(%Qubit*) + + declare void @__quantum__qis__reset__body(%Qubit*) + + declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*) #1 + + declare void @__quantum__rt__tuple_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="2" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10} + + !0 = !{i32 1, !"qir_major_version", i32 1} + !1 = !{i32 7, !"qir_minor_version", i32 0} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 1, !"classical_ints", i1 true} + !5 = !{i32 1, !"qubit_resetting", i1 true} + !6 = !{i32 1, !"classical_floats", i1 false} + !7 = !{i32 1, !"backwards_branching", i1 false} + !8 = !{i32 1, !"classical_fixed_points", i1 false} + !9 = !{i32 1, !"user_functions", i1 false} + !10 = !{i32 1, !"multiple_target_branching", i1 false} + "#]].assert_eq(&qir); + } + + #[test] + fn qubit_id_swap_with_out_of_order_release_uses_correct_ids() { + let source = "namespace Test { + @EntryPoint() + operation Main() : (Result, Result) { + let q0 = QIR.Runtime.__quantum__rt__qubit_allocate(); + let q1 = QIR.Runtime.__quantum__rt__qubit_allocate(); + let q2 = QIR.Runtime.__quantum__rt__qubit_allocate(); + X(q0); + X(q1); + X(q2); + SwapLabels(q0, q1); + QIR.Runtime.__quantum__rt__qubit_release(q0); + let q3 = QIR.Runtime.__quantum__rt__qubit_allocate(); + X(q3); + (MResetZ(q3), MResetZ(q1)) + } + }"; + let sources = SourceMap::new([("test.qs".into(), source.into())], None); + let language_features = LanguageFeatures::default(); + let capabilities = TargetCapabilityFlags::Adaptive + | TargetCapabilityFlags::QubitReset + | TargetCapabilityFlags::IntegerComputations; + + let (std_id, store) = crate::compile::package_store_with_stdlib(capabilities); + let qir = get_qir( + sources, + language_features, + capabilities, + store, + &[(std_id, None)], + ) + .expect("Failed to generate QIR"); + expect![[r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 2 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__rt__tuple_record_output(i64 2, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__x__body(%Qubit*) + + declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*) #1 + + declare void @__quantum__rt__tuple_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="2" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10} + + !0 = !{i32 1, !"qir_major_version", i32 1} + !1 = !{i32 7, !"qir_minor_version", i32 0} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 1, !"classical_ints", i1 true} + !5 = !{i32 1, !"qubit_resetting", i1 true} + !6 = !{i32 1, !"classical_floats", i1 false} + !7 = !{i32 1, !"backwards_branching", i1 false} + !8 = !{i32 1, !"classical_fixed_points", i1 false} + !9 = !{i32 1, !"user_functions", i1 false} + !10 = !{i32 1, !"multiple_target_branching", i1 false} + "#]].assert_eq(&qir); + } + #[test] fn dynamic_integer_with_branch_and_phi_supported() { let source = "namespace Test { diff --git a/compiler/qsc_circuit/src/builder.rs b/compiler/qsc_circuit/src/builder.rs index c40d8d9be6..17d78b3bc2 100644 --- a/compiler/qsc_circuit/src/builder.rs +++ b/compiler/qsc_circuit/src/builder.rs @@ -7,7 +7,6 @@ use crate::{ }; use num_bigint::BigUint; use num_complex::Complex; -use qsc_codegen::remapper::{HardwareId, Remapper}; use qsc_data_structures::index_map::IndexMap; use qsc_eval::{backend::Backend, val::Value}; use std::{fmt::Write, mem::take, rc::Rc}; @@ -181,6 +180,10 @@ impl Backend for Builder { self.remapper.qubit_release(q); } + fn qubit_swap_id(&mut self, q0: usize, q1: usize) { + self.remapper.swap(q0, q1); + } + fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { (Vec::new(), 0) } @@ -370,6 +373,97 @@ impl Builder { } } +/// Provides support for qubit id allocation, measurement and +/// reset operations for Base Profile targets. +/// +/// Since qubit reuse is disallowed, a mapping is maintained +/// from allocated qubit ids to hardware qubit ids. Each time +/// a qubit is reset, it is remapped to a fresh hardware qubit. +/// +/// Note that even though qubit reset & reuse is disallowed, +/// qubit ids are still reused for new allocations. +/// Measurements are tracked and deferred. +#[derive(Default)] +pub struct Remapper { + next_meas_id: usize, + next_qubit_id: usize, + next_qubit_hardware_id: HardwareId, + qubit_map: IndexMap, + measurements: Vec<(HardwareId, usize)>, +} + +impl Remapper { + pub fn map(&mut self, qubit: usize) -> HardwareId { + if let Some(mapped) = self.qubit_map.get(qubit) { + *mapped + } else { + let mapped = self.next_qubit_hardware_id; + self.next_qubit_hardware_id.0 += 1; + self.qubit_map.insert(qubit, mapped); + mapped + } + } + + pub fn m(&mut self, q: usize) -> usize { + let mapped_q = self.map(q); + let id = self.get_meas_id(); + self.measurements.push((mapped_q, id)); + id + } + + pub fn mreset(&mut self, q: usize) -> usize { + let id = self.m(q); + self.reset(q); + id + } + + pub fn reset(&mut self, q: usize) { + self.qubit_map.remove(q); + } + + pub fn qubit_allocate(&mut self) -> usize { + let id = self.next_qubit_id; + self.next_qubit_id += 1; + let _ = self.map(id); + id + } + + pub fn qubit_release(&mut self, _q: usize) { + self.next_qubit_id -= 1; + } + + pub fn swap(&mut self, q0: usize, q1: usize) { + let q0_mapped = self.map(q0); + let q1_mapped = self.map(q1); + self.qubit_map.insert(q0, q1_mapped); + self.qubit_map.insert(q1, q0_mapped); + } + + pub fn measurements(&self) -> impl Iterator { + self.measurements.iter() + } + + #[must_use] + pub fn num_qubits(&self) -> usize { + self.next_qubit_hardware_id.0 + } + + #[must_use] + pub fn num_measurements(&self) -> usize { + self.next_meas_id + } + + #[must_use] + fn get_meas_id(&mut self) -> usize { + let id = self.next_meas_id; + self.next_meas_id += 1; + id + } +} + +#[derive(Copy, Clone, Default)] +pub struct HardwareId(pub usize); + #[allow(clippy::unicode_not_nfc)] static KET_ZERO: &str = "|0〉"; diff --git a/compiler/qsc_codegen/src/lib.rs b/compiler/qsc_codegen/src/lib.rs index b4023885f0..c577611898 100644 --- a/compiler/qsc_codegen/src/lib.rs +++ b/compiler/qsc_codegen/src/lib.rs @@ -3,4 +3,3 @@ pub mod qir; pub mod qsharp; -pub mod remapper; diff --git a/compiler/qsc_codegen/src/remapper.rs b/compiler/qsc_codegen/src/remapper.rs deleted file mode 100644 index d2f506a590..0000000000 --- a/compiler/qsc_codegen/src/remapper.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use qsc_data_structures::index_map::IndexMap; - -/// Provides support for qubit id allocation, measurement and -/// reset operations for Base Profile targets. -/// -/// Since qubit reuse is disallowed, a mapping is maintained -/// from allocated qubit ids to hardware qubit ids. Each time -/// a qubit is reset, it is remapped to a fresh hardware qubit. -/// -/// Note that even though qubit reset & reuse is disallowed, -/// qubit ids are still reused for new allocations. -/// Measurements are tracked and deferred. -#[derive(Default)] -pub struct Remapper { - next_meas_id: usize, - next_qubit_id: usize, - next_qubit_hardware_id: HardwareId, - qubit_map: IndexMap, - measurements: Vec<(HardwareId, usize)>, -} - -impl Remapper { - pub fn map(&mut self, qubit: usize) -> HardwareId { - if let Some(mapped) = self.qubit_map.get(qubit) { - *mapped - } else { - let mapped = self.next_qubit_hardware_id; - self.next_qubit_hardware_id.0 += 1; - self.qubit_map.insert(qubit, mapped); - mapped - } - } - - pub fn m(&mut self, q: usize) -> usize { - let mapped_q = self.map(q); - let id = self.get_meas_id(); - self.measurements.push((mapped_q, id)); - id - } - - pub fn mreset(&mut self, q: usize) -> usize { - let id = self.m(q); - self.reset(q); - id - } - - pub fn reset(&mut self, q: usize) { - self.qubit_map.remove(q); - } - - pub fn qubit_allocate(&mut self) -> usize { - let id = self.next_qubit_id; - self.next_qubit_id += 1; - let _ = self.map(id); - id - } - - pub fn qubit_release(&mut self, _q: usize) { - self.next_qubit_id -= 1; - } - - pub fn measurements(&self) -> impl Iterator { - self.measurements.iter() - } - - #[must_use] - pub fn num_qubits(&self) -> usize { - self.next_qubit_hardware_id.0 - } - - #[must_use] - pub fn num_measurements(&self) -> usize { - self.next_meas_id - } - - #[must_use] - fn get_meas_id(&mut self) -> usize { - let id = self.next_meas_id; - self.next_meas_id += 1; - id - } -} - -#[derive(Copy, Clone, Default)] -pub struct HardwareId(pub usize); diff --git a/compiler/qsc_eval/src/backend.rs b/compiler/qsc_eval/src/backend.rs index b281ed9448..b1fbab68b5 100644 --- a/compiler/qsc_eval/src/backend.rs +++ b/compiler/qsc_eval/src/backend.rs @@ -85,17 +85,18 @@ pub trait Backend { fn qubit_release(&mut self, _q: usize) { unimplemented!("qubit_release operation"); } + fn qubit_swap_id(&mut self, _q0: usize, _q1: usize) { + unimplemented!("qubit_swap_id operation"); + } fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { unimplemented!("capture_quantum_state operation"); } fn qubit_is_zero(&mut self, _q: usize) -> bool { unimplemented!("qubit_is_zero operation"); } - fn custom_intrinsic(&mut self, _name: &str, _arg: Value) -> Option> { None } - fn set_seed(&mut self, _seed: Option) {} } @@ -240,6 +241,10 @@ impl Backend for SparseSim { self.sim.release(q); } + fn qubit_swap_id(&mut self, q0: usize, q1: usize) { + self.sim.swap_qubit_ids(q0, q1); + } + fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { let (state, count) = self.sim.get_state(); // Because the simulator returns the state indices with opposite endianness from the @@ -458,6 +463,11 @@ where self.main.qubit_release(q); } + fn qubit_swap_id(&mut self, q0: usize, q1: usize) { + self.chained.qubit_swap_id(q0, q1); + self.main.qubit_swap_id(q0, q1); + } + fn capture_quantum_state( &mut self, ) -> (Vec<(num_bigint::BigUint, num_complex::Complex)>, usize) { diff --git a/compiler/qsc_eval/src/intrinsic.rs b/compiler/qsc_eval/src/intrinsic.rs index 7b5a3420ce..3f93137cd4 100644 --- a/compiler/qsc_eval/src/intrinsic.rs +++ b/compiler/qsc_eval/src/intrinsic.rs @@ -133,6 +133,11 @@ pub(crate) fn call( Err(Error::ReleasedQubitNotZero(qubit, arg_span)) } } + "__quantum__rt__qubit_swap_ids" => { + let [q0, q1] = unwrap_tuple(arg); + sim.qubit_swap_id(q0.unwrap_qubit().0, q1.unwrap_qubit().0); + Ok(Value::unit()) + } "__quantum__qis__ccx__body" => { three_qubit_gate(|ctl0, ctl1, q| sim.ccx(ctl0, ctl1, q), arg, arg_span) } diff --git a/compiler/qsc_eval/src/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs index 1746c0578f..dd707a76fe 100644 --- a/compiler/qsc_eval/src/intrinsic/tests.rs +++ b/compiler/qsc_eval/src/intrinsic/tests.rs @@ -127,6 +127,10 @@ impl Backend for CustomSim { self.sim.qubit_release(q); } + fn qubit_swap_id(&mut self, q0: usize, q1: usize) { + self.sim.qubit_swap_id(q0, q1); + } + fn capture_quantum_state( &mut self, ) -> (Vec<(num_bigint::BigUint, num_complex::Complex)>, usize) { diff --git a/compiler/qsc_eval/src/tests.rs b/compiler/qsc_eval/src/tests.rs index f392286e1d..1752962c23 100644 --- a/compiler/qsc_eval/src/tests.rs +++ b/compiler/qsc_eval/src/tests.rs @@ -418,23 +418,23 @@ fn block_qubit_use_array_invalid_count_expr() { 0, ), span: Span { - lo: 2064, - hi: 2121, + lo: 2172, + hi: 2229, }, }, ), [ Frame { span: Span { - lo: 2064, - hi: 2121, + lo: 2172, + hi: 2229, }, id: StoreItemId { package: PackageId( 0, ), item: LocalItemId( - 6, + 7, ), }, caller: PackageId( @@ -3724,7 +3724,7 @@ fn controlled_operation_with_duplicate_controls_fails() { 1, ), item: LocalItemId( - 130, + 131, ), }, caller: PackageId( @@ -3774,7 +3774,7 @@ fn controlled_operation_with_target_in_controls_fails() { 1, ), item: LocalItemId( - 130, + 131, ), }, caller: PackageId( diff --git a/compiler/qsc_partial_eval/src/lib.rs b/compiler/qsc_partial_eval/src/lib.rs index ee8f234958..588ee02aec 100644 --- a/compiler/qsc_partial_eval/src/lib.rs +++ b/compiler/qsc_partial_eval/src/lib.rs @@ -54,7 +54,7 @@ use qsc_rir::{ }, }; use rustc_hash::FxHashMap; -use std::{collections::hash_map::Entry, rc::Rc, result::Result}; +use std::{array, collections::hash_map::Entry, rc::Rc, result::Result}; use thiserror::Error; /// Partially evaluates a program with the specified entry expression. @@ -568,7 +568,7 @@ impl<'a> PartialEvaluator<'a> { // Create the operands. let lhs_operand = Operand::Literal(Literal::Bool(lhs_bool)); - let rhs_operand = map_eval_value_to_rir_operand(&rhs_value); + let rhs_operand = self.map_eval_value_to_rir_operand(&rhs_value); // If both operands are literals, evaluate the binary operation and return its value. if let (Operand::Literal(lhs_literal), Operand::Literal(rhs_literal)) = @@ -657,7 +657,7 @@ impl<'a> PartialEvaluator<'a> { self.get_expr_package_span(rhs_expr_id), )); }; - let rhs_operand = map_eval_value_to_rir_operand(&rhs_value); + let rhs_operand = self.map_eval_value_to_rir_operand(&rhs_value); // Get the comparison result depending on the operator and the RHS value. let result_var = match (bin_op, rhs_operand) { @@ -733,7 +733,7 @@ impl<'a> PartialEvaluator<'a> { self.get_expr_package_span(rhs_expr_id), )); }; - let rhs_operand = map_eval_value_to_rir_operand(&rhs_value); + let rhs_operand = self.map_eval_value_to_rir_operand(&rhs_value); // Store the RHS value into the the variable that represents the result of the Boolean operation. let store_ins = Instruction::Store(rhs_operand, result_rir_var); @@ -783,7 +783,7 @@ impl<'a> PartialEvaluator<'a> { self.get_expr_package_span(rhs_expr_id), )); }; - let rhs_operand = map_eval_value_to_rir_operand(&rhs_value); + let rhs_operand = self.map_eval_value_to_rir_operand(&rhs_value); assert!( matches!(rhs_operand.get_type(), rir::Ty::Integer), "LHS value is expected to be of integer type" @@ -1290,6 +1290,7 @@ impl<'a> PartialEvaluator<'a> { // Qubit allocations and measurements have special handling. "__quantum__rt__qubit_allocate" => Ok(self.allocate_qubit()), "__quantum__rt__qubit_release" => Ok(self.release_qubit(args_value)), + "__quantum__rt__qubit_swap_ids" => Ok(self.swap_qubit_ids(args_value)), "__quantum__qis__m__body" => Ok(self.measure_qubit(builder::m_decl(), args_value)), "__quantum__qis__mresetz__body" => { Ok(self.measure_qubit(builder::mresetz_decl(), args_value)) @@ -1359,7 +1360,7 @@ impl<'a> PartialEvaluator<'a> { ); let args_operands = args .into_iter() - .map(|arg| map_eval_value_to_rir_operand(&arg.into_value())) + .map(|arg| self.map_eval_value_to_rir_operand(&arg.into_value())) .collect(); let instruction = Instruction::Call(callable_id, args_operands, None); @@ -1516,7 +1517,7 @@ impl<'a> PartialEvaluator<'a> { // If there is a variable to save the value of the if expression to, add a store instruction. if let Some(if_expr_var) = if_expr_var { - let body_operand = map_eval_value_to_rir_operand(&body_control.into_value()); + let body_operand = self.map_eval_value_to_rir_operand(&body_control.into_value()); let store_ins = Instruction::Store(body_operand, if_expr_var); self.get_current_rir_block_mut().0.push(store_ins); } @@ -1673,7 +1674,7 @@ impl<'a> PartialEvaluator<'a> { }; // Generate the instruction depending on the unary operator. - let value_operand = map_eval_value_to_rir_operand(&value); + let value_operand = self.map_eval_value_to_rir_operand(&value); let instruction = match un_op { UnOp::Neg => { let constant = match rir_variable_type { @@ -2184,7 +2185,7 @@ impl<'a> PartialEvaluator<'a> { .insert_hybrid_local_value(local_var_id, Value::Var(eval_var)); // Insert a store instruction. - let value_operand = map_eval_value_to_rir_operand(value); + let value_operand = self.map_eval_value_to_rir_operand(value); let rir_var = map_eval_var_to_rir_var(eval_var); let store_ins = Instruction::Store(value_operand, rir_var); self.get_current_rir_block_mut().0.push(store_ins); @@ -2234,9 +2235,9 @@ impl<'a> PartialEvaluator<'a> { // Get the qubit and result IDs to use in the qubit measure instruction. let qubit = args_value.unwrap_qubit(); let qubit_value = Value::Qubit(qubit); - let qubit_operand = map_eval_value_to_rir_operand(&qubit_value); + let qubit_operand = self.map_eval_value_to_rir_operand(&qubit_value); let result_value = Value::Result(self.resource_manager.next_result_register()); - let result_operand = map_eval_value_to_rir_operand(&result_value); + let result_operand = self.map_eval_value_to_rir_operand(&result_value); // Check if the callable has already been added to the program and if not do so now. let measure_callable_id = self.get_or_insert_callable(measure_callable); @@ -2257,6 +2258,15 @@ impl<'a> PartialEvaluator<'a> { Value::unit() } + fn swap_qubit_ids(&mut self, args_value: Value) -> Value { + let tuple = args_value.unwrap_tuple(); + let [q0, q1] = array::from_fn(|i| tuple[i].clone()); + let (q0, q1) = (q0.unwrap_qubit(), q1.unwrap_qubit()); + self.resource_manager.swap_qubit_ids(q0, q1); + + Value::unit() + } + fn resolve_args( &self, store_pat_id: StorePatId, @@ -2486,7 +2496,7 @@ impl<'a> PartialEvaluator<'a> { .get_hybrid_local_value(local_var_id); if let Value::Var(var) = bound_value { // Insert a store instruction when the value of a variable is updated. - let rhs_operand = map_eval_value_to_rir_operand(&value); + let rhs_operand = self.map_eval_value_to_rir_operand(&value); let rir_var = map_eval_var_to_rir_var(*var); let store_ins = Instruction::Store(rhs_operand, rir_var); self.get_current_rir_block_mut().0.push(store_ins); @@ -2762,6 +2772,30 @@ impl<'a> PartialEvaluator<'a> { self.program.callables.insert(callable_id, callable); callable_id } + + fn map_eval_value_to_rir_operand(&self, value: &Value) -> Operand { + match value { + Value::Bool(b) => Operand::Literal(Literal::Bool(*b)), + Value::Double(d) => Operand::Literal(Literal::Double(*d)), + Value::Int(i) => Operand::Literal(Literal::Integer(*i)), + Value::Qubit(q) => Operand::Literal(Literal::Qubit( + self.resource_manager + .map_qubit(*q) + .try_into() + .expect("could not convert qubit ID to u32"), + )), + Value::Result(r) => match r { + val::Result::Id(id) => Operand::Literal(Literal::Result( + (*id) + .try_into() + .expect("could not convert result ID to u32"), + )), + val::Result::Val(bool) => Operand::Literal(Literal::Bool(*bool)), + }, + Value::Var(var) => Operand::Variable(map_eval_var_to_rir_var(*var)), + _ => panic!("{value} cannot be mapped to a RIR operand"), + } + } } fn eval_un_op_with_literals(un_op: UnOp, value: Value) -> Value { @@ -2906,27 +2940,6 @@ fn get_spec_decl(spec_impl: &SpecImpl, functor_app: FunctorApp) -> &SpecDecl { } } -fn map_eval_value_to_rir_operand(value: &Value) -> Operand { - match value { - Value::Bool(b) => Operand::Literal(Literal::Bool(*b)), - Value::Double(d) => Operand::Literal(Literal::Double(*d)), - Value::Int(i) => Operand::Literal(Literal::Integer(*i)), - Value::Qubit(q) => Operand::Literal(Literal::Qubit( - q.0.try_into().expect("could not convert qubit ID to u32"), - )), - Value::Result(r) => match r { - val::Result::Id(id) => Operand::Literal(Literal::Result( - (*id) - .try_into() - .expect("could not convert result ID to u32"), - )), - val::Result::Val(bool) => Operand::Literal(Literal::Bool(*bool)), - }, - Value::Var(var) => Operand::Variable(map_eval_var_to_rir_var(*var)), - _ => panic!("{value} cannot be mapped to a RIR operand"), - } -} - fn map_eval_var_to_rir_var(var: Var) -> rir::Variable { rir::Variable { variable_id: var.id.into(), diff --git a/compiler/qsc_partial_eval/src/management.rs b/compiler/qsc_partial_eval/src/management.rs index 5325b905ab..8d211a99de 100644 --- a/compiler/qsc_partial_eval/src/management.rs +++ b/compiler/qsc_partial_eval/src/management.rs @@ -3,6 +3,7 @@ use num_bigint::BigUint; use num_complex::Complex; +use qsc_data_structures::index_map::IndexMap; use qsc_eval::{ backend::Backend, val::{Qubit, Result, Value}, @@ -13,6 +14,7 @@ use qsc_rir::rir::{BlockId, CallableId, VariableId}; #[derive(Default)] pub struct ResourceManager { qubits_in_use: Vec, + qubit_id_map: IndexMap, next_callable: CallableId, next_block: BlockId, next_result_register: usize, @@ -20,6 +22,13 @@ pub struct ResourceManager { } impl ResourceManager { + pub fn map_qubit(&self, q: Qubit) -> usize { + *self + .qubit_id_map + .get(q.0) + .expect("qubit id should be in map") + } + /// Count of qubits used. pub fn qubit_count(&self) -> usize { self.qubits_in_use.len() @@ -32,19 +41,29 @@ impl ResourceManager { /// Allocates a qubit by favoring available qubit IDs before using new ones. pub fn allocate_qubit(&mut self) -> Qubit { - if let Some(qubit_id) = self.qubits_in_use.iter().position(|in_use| !in_use) { - self.qubits_in_use[qubit_id] = true; - Qubit(qubit_id) + let qubit = if let Some(qubit) = self.qubits_in_use.iter().position(|in_use| !in_use) { + self.qubits_in_use[qubit] = true; + qubit } else { self.qubits_in_use.push(true); - let qubit_id = self.qubits_in_use.len() - 1; - Qubit(qubit_id) + self.qubits_in_use.len() - 1 + }; + let mut next_id = 0; + loop { + if !self.qubit_id_map.contains_key(next_id) { + self.qubit_id_map.insert(next_id, qubit); + break; + } + next_id += 1; } + Qubit(next_id) } /// Releases a qubit ID for future use. pub fn release_qubit(&mut self, q: Qubit) { - self.qubits_in_use[q.0] = false; + let qubit = self.map_qubit(q); + self.qubit_id_map.remove(q.0); + self.qubits_in_use[qubit] = false; } /// Gets the next block ID. @@ -74,6 +93,13 @@ impl ResourceManager { self.next_var += 1; var_id.into() } + + pub fn swap_qubit_ids(&mut self, q0: Qubit, q1: Qubit) { + let id0 = self.map_qubit(q0); + let id1 = self.map_qubit(q1); + self.qubit_id_map.insert(q0.0, id1); + self.qubit_id_map.insert(q1.0, id0); + } } /// Custom backend meant to panic when most of its methods are called. diff --git a/compiler/qsc_passes/src/replace_qubit_allocation/tests.rs b/compiler/qsc_passes/src/replace_qubit_allocation/tests.rs index 5a86d304bc..91137294a2 100644 --- a/compiler/qsc_passes/src/replace_qubit_allocation/tests.rs +++ b/compiler/qsc_passes/src/replace_qubit_allocation/tests.rs @@ -88,13 +88,13 @@ fn test_qubit_array() { Stmt 18 [55-72]: Local (Immutable): Pat 19 [55-72] [Type Qubit[]]: Bind: Ident 7 [55-72] "q" Expr 16 [55-72] [Type Qubit[]]: Call: - Expr 15 [55-72] [Type (Int => Qubit[])]: Var: Item 6 (Package 0) + Expr 15 [55-72] [Type (Int => Qubit[])]: Var: Item 7 (Package 0) Expr 9 [69-70] [Type Int]: Lit: Int(3) Stmt 10 [81-91]: Local (Immutable): Pat 11 [85-86] [Type Int]: Bind: Ident 12 [85-86] "x" Expr 13 [89-90] [Type Int]: Lit: Int(3) Stmt 21 [0-0]: Semi: Expr 22 [0-0] [Type Unit]: Call: - Expr 20 [55-72] [Type (Qubit[] => Unit)]: Var: Item 7 (Package 0) + Expr 20 [55-72] [Type (Qubit[] => Unit)]: Var: Item 8 (Package 0) Expr 23 [55-72] [Type Qubit[]]: Var: Local 7 adj: ctl: @@ -184,7 +184,7 @@ fn test_multiple_qubits_tuple() { Stmt 33 [78-86]: Local (Immutable): Pat 34 [78-86] [Type Qubit[]]: Bind: Ident 22 [78-86] "@generated_ident_22" Expr 31 [78-86] [Type Qubit[]]: Call: - Expr 30 [78-86] [Type (Int => Qubit[])]: Var: Item 6 (Package 0) + Expr 30 [78-86] [Type (Int => Qubit[])]: Var: Item 7 (Package 0) Expr 14 [84-85] [Type Int]: Lit: Int(3) Stmt 35 [55-88]: Local (Immutable): Pat 6 [59-65] [Type (Qubit, Qubit[])]: Tuple: @@ -197,7 +197,7 @@ fn test_multiple_qubits_tuple() { Pat 16 [101-102] [Type Int]: Bind: Ident 17 [101-102] "x" Expr 18 [105-106] [Type Int]: Lit: Int(3) Stmt 37 [0-0]: Semi: Expr 38 [0-0] [Type Unit]: Call: - Expr 36 [78-86] [Type (Qubit[] => Unit)]: Var: Item 7 (Package 0) + Expr 36 [78-86] [Type (Qubit[] => Unit)]: Var: Item 8 (Package 0) Expr 39 [78-86] [Type Qubit[]]: Var: Local 22 Stmt 41 [0-0]: Semi: Expr 42 [0-0] [Type Unit]: Call: Expr 40 [69-76] [Type (Qubit => Unit)]: Var: Item 5 (Package 0) @@ -701,7 +701,7 @@ fn test_array_expr() { Stmt 39 [55-125]: Local (Immutable): Pat 40 [55-125] [Type Qubit[]]: Bind: Ident 7 [55-125] "a" Expr 37 [55-125] [Type Qubit[]]: Call: - Expr 36 [55-125] [Type (Int => Qubit[])]: Var: Item 6 (Package 0) + Expr 36 [55-125] [Type (Int => Qubit[])]: Var: Item 7 (Package 0) Expr 9 [69-123] [Type Int]: Expr Block: Block 10 [69-123] [Type Int]: Stmt 25 [83-99]: Local (Immutable): Pat 26 [83-99] [Type Qubit]: Bind: Ident 13 [83-99] "b" @@ -719,7 +719,7 @@ fn test_array_expr() { Pat 18 [138-139] [Type Int]: Bind: Ident 19 [138-139] "x" Expr 20 [142-143] [Type Int]: Lit: Int(3) Stmt 42 [0-0]: Semi: Expr 43 [0-0] [Type Unit]: Call: - Expr 41 [55-125] [Type (Qubit[] => Unit)]: Var: Item 7 (Package 0) + Expr 41 [55-125] [Type (Qubit[] => Unit)]: Var: Item 8 (Package 0) Expr 44 [55-125] [Type Qubit[]]: Var: Local 7 adj: ctl: diff --git a/library/core/qir.qs b/library/core/qir.qs index 25343d95ce..a833650ff6 100644 --- a/library/core/qir.qs +++ b/library/core/qir.qs @@ -10,6 +10,10 @@ namespace QIR.Runtime { body intrinsic; } + operation __quantum__rt__qubit_swap_ids(q0 : Qubit, q1 : Qubit) : Unit { + body intrinsic; + } + operation AllocateQubitArray(size : Int) : Qubit[] { if size < 0 { fail "Cannot allocate qubit array with a negative length"; @@ -27,5 +31,5 @@ namespace QIR.Runtime { } } - export __quantum__rt__qubit_allocate, __quantum__rt__qubit_release, AllocateQubitArray, ReleaseQubitArray; + export __quantum__rt__qubit_allocate, __quantum__rt__qubit_release, __quantum__rt__qubit_swap_ids, AllocateQubitArray, ReleaseQubitArray; } diff --git a/library/src/tests/canon.rs b/library/src/tests/canon.rs index 1c30e5a37f..5eaf46e959 100644 --- a/library/src/tests/canon.rs +++ b/library/src/tests/canon.rs @@ -107,6 +107,19 @@ fn check_fst_snd() { test_expression("Snd(7,6)", &Value::Int(6)); } +#[test] +fn check_swap_labels() { + test_expression( + "{ + use qs = Qubit[2]; + X(qs[0]); + SwapLabels(qs[0], qs[1]); + MResetEachZ(qs) + }", + &Value::Array(vec![Value::RESULT_ZERO, Value::RESULT_ONE].into()), + ); +} + #[test] fn check_apply_cnot_chain_2() { test_expression( diff --git a/library/std/src/canon.qs b/library/std/src/canon.qs index 6bccc1c1fa..82e29dcda6 100644 --- a/library/std/src/canon.qs +++ b/library/std/src/canon.qs @@ -3,6 +3,7 @@ namespace Microsoft.Quantum.Canon { open QIR.Intrinsic; + open QIR.Runtime; open Microsoft.Quantum.Intrinsic; open Microsoft.Quantum.Diagnostics; open Microsoft.Quantum.Math; @@ -246,6 +247,24 @@ namespace Microsoft.Quantum.Canon { return snd; } + /// # Summary + /// Exchanges the labels of two qubits. This can function like a SWAP gate, but without needing to + /// perform any operations on the qubits themselves. The relabeling is done purely in qubit ID management. + /// + /// # Input + /// ## q0 + /// The first qubit to swap. + /// ## q1 + /// The second qubit to swap. + /// + /// # Remarks + /// This operation is useful when you need to swap qubits in a way that does not incur any quantum operations. + /// Note that when compiling for execution on hardware with limited qubit connectivity, this operation + /// may not result in any changes to qubit adjacency and a SWAP gate may still be required. + operation SwapLabels(q0 : Qubit, q1 : Qubit) : Unit { + __quantum__rt__qubit_swap_ids(q0, q1); + } + /// # Summary /// Computes the parity of a register of qubits in-place. /// @@ -584,6 +603,6 @@ namespace Microsoft.Quantum.Canon { adjoint self; } - export ApplyToEach, ApplyToEachA, ApplyToEachC, ApplyToEachCA, CX, CY, CZ, Fst, Snd, ApplyCNOTChain, ApplyP, ApplyPauli, ApplyPauliFromBitString, ApplyPauliFromInt, ApplyControlledOnInt, ApplyControlledOnBitString, ApplyQFT, SwapReverseRegister, ApplyXorInPlace, ApplyXorInPlaceL; + export ApplyToEach, ApplyToEachA, ApplyToEachC, ApplyToEachCA, CX, CY, CZ, Fst, Snd, SwapLabels, ApplyCNOTChain, ApplyP, ApplyPauli, ApplyPauliFromBitString, ApplyPauliFromInt, ApplyControlledOnInt, ApplyControlledOnBitString, ApplyQFT, SwapReverseRegister, ApplyXorInPlace, ApplyXorInPlaceL; } diff --git a/pip/tests/test_interpreter.py b/pip/tests/test_interpreter.py index 86838e1899..a3b886e6db 100644 --- a/pip/tests/test_interpreter.py +++ b/pip/tests/test_interpreter.py @@ -274,6 +274,20 @@ def test_entry_expr_circuit() -> None: ) +def test_swap_label_circuit() -> None: + e = Interpreter(TargetProfile.Unrestricted) + e.interpret( + "operation Foo() : Unit { use q1 = Qubit(); use q2 = Qubit(); X(q1); SwapLabels(q1, q2); X(q2); }" + ) + circuit = e.circuit("Foo()") + assert str(circuit) == dedent( + """\ + q_0 ── X ──── X ── + q_1 ────────────── + """ + ) + + def test_callables_failing_profile_validation_are_not_registered() -> None: e = Interpreter(TargetProfile.Adaptive_RI) with pytest.raises(Exception) as excinfo: diff --git a/resource_estimator/src/counts.rs b/resource_estimator/src/counts.rs index 3e9aa685da..f25ad2b950 100644 --- a/resource_estimator/src/counts.rs +++ b/resource_estimator/src/counts.rs @@ -506,6 +506,11 @@ impl Backend for LogicalCounter { self.free_list.push(q); } + fn qubit_swap_id(&mut self, _q0: usize, _q1: usize) { + // This can safely be treated as a no-op, because counts don't care which qubit is operated on, + // just how many operations are performed, and relabeling is non-physical. + } + fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { (Vec::new(), 0) } diff --git a/wasm/src/tests.rs b/wasm/src/tests.rs index e470e89804..8a5f8bfed0 100644 --- a/wasm/src/tests.rs +++ b/wasm/src/tests.rs @@ -456,7 +456,7 @@ fn test_runtime_error_default_span() { 1, ) .expect("code should compile and run"); - expect![[r#"{"result":{"code":"Qsc.Eval.UserFail","message":"runtime error: program failed: Cannot allocate qubit array with a negative length","range":{"end":{"character":1,"line":0},"start":{"character":0,"line":0}},"related":[{"location":{"source":"qsharp-library-source:core/qir.qs","span":{"end":{"character":69,"line":14},"start":{"character":12,"line":14}}},"message":"explicit fail"}],"severity":"error"},"success":false,"type":"Result"}"#]] + expect![[r#"{"result":{"code":"Qsc.Eval.UserFail","message":"runtime error: program failed: Cannot allocate qubit array with a negative length","range":{"end":{"character":1,"line":0},"start":{"character":0,"line":0}},"related":[{"location":{"source":"qsharp-library-source:core/qir.qs","span":{"end":{"character":69,"line":18},"start":{"character":12,"line":18}}},"message":"explicit fail"}],"severity":"error"},"success":false,"type":"Result"}"#]] .assert_eq(&output.join("\n")); }