From d72af2dfa3381de212b43d039c3d683b623660a8 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 25 Aug 2026 16:26:00 -0700 Subject: [PATCH 01/18] support remaining non-clifford gates in stim --- source/compiler/stim_compiler/src/qir.rs | 178 ++++++++++++++++++++--- 1 file changed, 161 insertions(+), 17 deletions(-) diff --git a/source/compiler/stim_compiler/src/qir.rs b/source/compiler/stim_compiler/src/qir.rs index 5df52ee74d1..b628b730c82 100644 --- a/source/compiler/stim_compiler/src/qir.rs +++ b/source/compiler/stim_compiler/src/qir.rs @@ -464,6 +464,13 @@ pub enum Error { #[label] span: Span, }, + #[error("instruction {instruction} requires a multiple of three targets")] + #[diagnostic(code("Qdk.Stim.Compiler.TargetCountNotMultipleOfThree"))] + TargetCountNotMultipleOfThree { + instruction: String, + #[label] + span: Span, + }, #[error("measurement record target in an unsupported position in instruction: {instruction}")] #[diagnostic(code("Qdk.Stim.Compiler.MisplacedMeasurementRecord"))] MisplacedMeasurementRecord { @@ -1419,10 +1426,60 @@ impl<'noise> Compiler<'noise> { // Non-Clifford Gates "T" => self.broadcast(instruction, |s, q| s.op("t", q)), "T_DAG" => self.broadcast(instruction, |s, q| s.op_adj("t", q)), + "TPP" | "TPP_DAG" => self.broadcast_pauli_product(instruction, |s, q, negated| { + let invert = (instruction.name == "TPP_DAG") ^ negated; + if invert { + s.op_adj("t", q); + } else { + s.op("t", q); + } + }), + "CH" => self.broadcast_pair(instruction, |s, q0, q1| { + // Clifft decomposition: R_Y(0.25 pi) 1; CX 0 1; R_Y(-0.25 pi) 1 + s.op_rotation("ry", 0.25 * PI, q1); + s.op_2("cx", q0, q1); + s.op_rotation("ry", -0.25 * PI, q1); + }), + "CCZ" => self.broadcast_triple(instruction, |s, q0, q1, q2| { + // Clifft decomposition: H 2; CCX 0 1 2; H 2 + s.op("h", q2); + s.op_3("ccx", q0, q1, q2); + s.op("h", q2); + }), + "CCX" => self.broadcast_triple(instruction, |s, q0, q1, q2| { + s.op_3("ccx", q0, q1, q2); + }), "R_X" | "R_Y" | "R_Z" => self.broadcast_rotation(instruction, |s, angle, q| { s.op_rotation(&instruction.name.to_lowercase().replace("_", ""), angle, q); }), - + "U3" | "U" => { + let Some(angles) = self.expect_angles(instruction, 3) else { + return; + }; + self.for_each_qubit(instruction, |s, q| { + s.op_rotation("rz", angles[2], q); + s.op_rotation("ry", angles[0], q); + s.op_rotation("rz", angles[1], q); + }); + } + "R_XX" | "R_YY" | "R_ZZ" => { + self.broadcast_pair_rotation(instruction, |s, angle, q0, q1| { + s.op_rotation_2( + &instruction.name.to_lowercase().replace("_", ""), + angle, + q0, + q1, + ); + }) + } + "R_PAULI" => { + let Some(angle) = self.expect_angle(instruction) else { + return; + }; + self.for_each_pauli_product(instruction, |s, q, negated| { + s.op_rotation("rz", if negated { -angle } else { angle }, q); + }); + } _ => self.unknown(instruction), } } @@ -1542,6 +1599,17 @@ impl<'noise> Compiler<'noise> { self.for_each_qubit(instruction, |s, q| operation(s, angle, q)); } + fn broadcast_pair_rotation( + &mut self, + instruction: &Instruction, + mut operation: impl FnMut(&mut Self, Radians, StimQubitId, StimQubitId), + ) { + let Some(angle) = self.expect_angle(instruction) else { + return; + }; + self.for_each_pair(instruction, |s, q0, q1| operation(s, angle, q0, q1)); + } + fn accumulate_correlated_noise(&mut self, instruction: &Instruction) { let Some(probability) = self.expect_arg(instruction) else { return; @@ -1604,6 +1672,28 @@ impl<'noise> Compiler<'noise> { } } + fn for_each_triple( + &mut self, + instruction: &Instruction, + mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, StimQubitId), + ) { + let Some(triples) = self.expect_target_triples(instruction) else { + return; + }; + for triple in triples { + let Some((q0, _)) = self.expect_qubit(instruction, &triple[0], false) else { + continue; + }; + let Some((q1, _)) = self.expect_qubit(instruction, &triple[1], false) else { + continue; + }; + let Some((q2, _)) = self.expect_qubit(instruction, &triple[2], false) else { + continue; + }; + operation(self, q0, q1, q2); + } + } + fn for_each_negatable_pair( &mut self, instruction: &Instruction, @@ -1632,6 +1722,15 @@ impl<'noise> Compiler<'noise> { self.for_each_pair(instruction, operation); } + fn broadcast_triple( + &mut self, + instruction: &Instruction, + operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, StimQubitId), + ) { + self.unsupported_args(instruction); + self.for_each_triple(instruction, operation); + } + fn broadcast_pair_measure( &mut self, instruction: &Instruction, @@ -1861,6 +1960,19 @@ impl<'noise> Compiler<'noise> { self.writer.write_qis_call(intrinsic, &[q]); } + fn op_2(&mut self, intrinsic: &str, q0: StimQubitId, q1: StimQubitId) { + let q0 = self.id_map.allocate_qubit(q0); + let q1 = self.id_map.allocate_qubit(q1); + self.writer.write_qis_call(intrinsic, &[q0, q1]); + } + + fn op_3(&mut self, intrinsic: &str, q0: StimQubitId, q1: StimQubitId, q2: StimQubitId) { + let q0 = self.id_map.allocate_qubit(q0); + let q1 = self.id_map.allocate_qubit(q1); + let q2 = self.id_map.allocate_qubit(q2); + self.writer.write_qis_call(intrinsic, &[q0, q1, q2]); + } + fn op_adj(&mut self, intrinsic: &str, qubit: StimQubitId) { let q = self.id_map.allocate_qubit(qubit); self.writer.write_qis_adj_call(intrinsic, &[q]); @@ -1897,12 +2009,6 @@ impl<'noise> Compiler<'noise> { r } - fn op_2(&mut self, intrinsic: &str, q0: StimQubitId, q1: StimQubitId) { - let q0 = self.id_map.allocate_qubit(q0); - let q1 = self.id_map.allocate_qubit(q1); - self.writer.write_qis_call(intrinsic, &[q0, q1]); - } - fn op_noise(&mut self, table: NoiseTable, qubits: &[StimQubitId]) { let ids: Vec = qubits .iter() @@ -1923,6 +2029,12 @@ impl<'noise> Compiler<'noise> { self.writer.write_rotation_call(intrinsic, angle, &[qubit]); } + fn op_rotation_2(&mut self, intrinsic: &str, angle: Radians, q0: StimQubitId, q1: StimQubitId) { + let q0 = self.id_map.allocate_qubit(q0); + let q1 = self.id_map.allocate_qubit(q1); + self.writer.write_rotation_call(intrinsic, angle, &[q0, q1]); + } + fn build_noise_table( &mut self, num_qubits: u32, @@ -2194,17 +2306,35 @@ impl<'noise> Compiler<'noise> { } fn expect_angle(&mut self, instruction: &Instruction) -> Option { - let angle: HalfTurns = self.expect_arg(instruction)?; - let radians = angle * PI; - if !radians.is_finite() { - self.push_error(Error::InvalidAngle { - instruction: instruction.name.clone(), - angle, - span: instruction.span, - }); - return None; + self.expect_angles(instruction, 1)?.pop() + } + + fn expect_angles( + &mut self, + instruction: &Instruction, + expected: usize, + ) -> Option> { + let angles: Vec = self.expect_args(instruction, expected)?; + let mut radians = Vec::with_capacity(angles.len()); + let mut has_invalid_angle = false; + for angle in angles { + let angle_in_radians = angle * PI; + if angle_in_radians.is_finite() { + radians.push(angle_in_radians); + } else { + self.push_error(Error::InvalidAngle { + instruction: instruction.name.clone(), + angle, + span: instruction.span, + }); + has_invalid_angle = true; + } + } + if !has_invalid_angle { + Some(radians) + } else { + None } - Some(radians) } fn expect_arg(&mut self, instruction: &Instruction) -> Option { @@ -2245,6 +2375,20 @@ impl<'noise> Compiler<'noise> { Some(instruction.targets.chunks(2)) } + fn expect_target_triples<'a>( + &mut self, + instruction: &'a Instruction, + ) -> Option> { + if !instruction.targets.len().is_multiple_of(3) { + self.push_error(Error::TargetCountNotMultipleOfThree { + instruction: instruction.name.clone(), + span: instruction.span, + }); + return None; + } + Some(instruction.targets.chunks(3)) + } + fn expect_readout_noise(&mut self, instruction: &Instruction) -> Option { if instruction.args.len() > 1 { self.push_error(Error::TooManyArgs { From 5c31eca9af5811ff4572a68670cbcb33a72e41ee Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 25 Aug 2026 16:38:17 -0700 Subject: [PATCH 02/18] reorganize functions within stim compiler --- source/compiler/stim_compiler/src/qir.rs | 284 +++++++++++------------ 1 file changed, 142 insertions(+), 142 deletions(-) diff --git a/source/compiler/stim_compiler/src/qir.rs b/source/compiler/stim_compiler/src/qir.rs index b628b730c82..a18fd4355b0 100644 --- a/source/compiler/stim_compiler/src/qir.rs +++ b/source/compiler/stim_compiler/src/qir.rs @@ -1531,144 +1531,41 @@ impl<'noise> Compiler<'noise> { } } - fn broadcast( - &mut self, - instruction: &Instruction, - operation: impl FnMut(&mut Self, StimQubitId), - ) { - self.unsupported_args(instruction); - self.for_each_qubit(instruction, operation); - } - - fn broadcast_measure( - &mut self, - instruction: &Instruction, - mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, - ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { - return; - }; - self.for_each_negatable_qubit(instruction, |s, q, negated| { - let result_id = measure(s, q, negated); - s.op_readout_noise(readout_noise, result_id); - }); - } - - fn broadcast_noise( - &mut self, - instruction: &Instruction, - mut noise: impl FnMut(&mut Self, StimQubitId, f64), - ) { - let Some(probability) = self.expect_arg(instruction) else { - return; - }; - self.for_each_qubit(instruction, |s, q| noise(s, q, probability)); - } - - fn broadcast_pauli_product( - &mut self, - instruction: &Instruction, - operation: impl FnMut(&mut Self, StimQubitId, bool), - ) { - self.unsupported_args(instruction); - self.for_each_pauli_product(instruction, operation); - } - - fn broadcast_pauli_product_measure( - &mut self, - instruction: &Instruction, - mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, - ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { - return; - }; - self.for_each_pauli_product(instruction, |s, q, negated| { - let result_id = measure(s, q, negated); - s.op_readout_noise(readout_noise, result_id); - }); - } - - fn broadcast_rotation( - &mut self, - instruction: &Instruction, - mut operation: impl FnMut(&mut Self, Radians, StimQubitId), - ) { - let Some(angle) = self.expect_angle(instruction) else { - return; - }; - self.for_each_qubit(instruction, |s, q| operation(s, angle, q)); - } - - fn broadcast_pair_rotation( + fn for_each_pair( &mut self, instruction: &Instruction, - mut operation: impl FnMut(&mut Self, Radians, StimQubitId, StimQubitId), + mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId), ) { - let Some(angle) = self.expect_angle(instruction) else { - return; - }; - self.for_each_pair(instruction, |s, q0, q1| operation(s, angle, q0, q1)); - } - - fn accumulate_correlated_noise(&mut self, instruction: &Instruction) { - let Some(probability) = self.expect_arg(instruction) else { + let Some(pairs) = self.expect_target_pairs(instruction) else { return; }; - let mut terms = Vec::with_capacity(instruction.targets.len()); - - for target in &instruction.targets { - let Some((fault, qubit)) = self.expect_fault_char(instruction, target) else { + for pair in pairs { + let Some((q0, _)) = self.expect_qubit(instruction, &pair[0], false) else { continue; }; - - terms.push((fault, qubit)); - } - - let row = CorrelatedRow { - probability, - terms, - span: instruction.span, - }; - - self.noise_accumulator.push_correlated_row(row); - } - - fn continue_correlated_noise(&mut self, instruction: &Instruction) { - if self.noise_accumulator.current_correlated_group.is_none() { - self.push_error(Error::OrphanedElseCorrelatedError { - span: instruction.span, - }); - return; - } - self.accumulate_correlated_noise(instruction); - } - - fn finish_correlated_noise(&mut self) { - if self.noise_accumulator.current_correlated_group.is_none() { - return; - } - match self.noise_accumulator.flush_correlated_group() { - Ok((noise_table, qubits)) => self.op_noise(noise_table, &qubits), - Err(error) => self.push_error(error), + let Some((q1, _)) = self.expect_qubit(instruction, &pair[1], false) else { + continue; + }; + operation(self, q0, q1); } } - fn for_each_pair( + fn for_each_negatable_pair( &mut self, instruction: &Instruction, - mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId), + mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, bool), ) { let Some(pairs) = self.expect_target_pairs(instruction) else { return; }; for pair in pairs { - let Some((q0, _)) = self.expect_qubit(instruction, &pair[0], false) else { + let Some((q0, neg0)) = self.expect_qubit(instruction, &pair[0], true) else { continue; }; - let Some((q1, _)) = self.expect_qubit(instruction, &pair[1], false) else { + let Some((q1, neg1)) = self.expect_qubit(instruction, &pair[1], true) else { continue; }; - operation(self, q0, q1); + operation(self, q0, q1, neg0 ^ neg1); } } @@ -1694,23 +1591,13 @@ impl<'noise> Compiler<'noise> { } } - fn for_each_negatable_pair( + fn broadcast( &mut self, instruction: &Instruction, - mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, bool), + operation: impl FnMut(&mut Self, StimQubitId), ) { - let Some(pairs) = self.expect_target_pairs(instruction) else { - return; - }; - for pair in pairs { - let Some((q0, neg0)) = self.expect_qubit(instruction, &pair[0], true) else { - continue; - }; - let Some((q1, neg1)) = self.expect_qubit(instruction, &pair[1], true) else { - continue; - }; - operation(self, q0, q1, neg0 ^ neg1); - } + self.unsupported_args(instruction); + self.for_each_qubit(instruction, operation); } fn broadcast_pair( @@ -1731,6 +1618,20 @@ impl<'noise> Compiler<'noise> { self.for_each_triple(instruction, operation); } + fn broadcast_measure( + &mut self, + instruction: &Instruction, + mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, + ) { + let Some(readout_noise) = self.expect_readout_noise(instruction) else { + return; + }; + self.for_each_negatable_qubit(instruction, |s, q, negated| { + let result_id = measure(s, q, negated); + s.op_readout_noise(readout_noise, result_id); + }); + } + fn broadcast_pair_measure( &mut self, instruction: &Instruction, @@ -1745,6 +1646,17 @@ impl<'noise> Compiler<'noise> { }); } + fn broadcast_noise( + &mut self, + instruction: &Instruction, + mut noise: impl FnMut(&mut Self, StimQubitId, f64), + ) { + let Some(probability) = self.expect_arg(instruction) else { + return; + }; + self.for_each_qubit(instruction, |s, q| noise(s, q, probability)); + } + fn broadcast_pair_noise( &mut self, instruction: &Instruction, @@ -1756,6 +1668,51 @@ impl<'noise> Compiler<'noise> { self.for_each_pair(instruction, |s, q0, q1| noise(s, q0, q1, probability)); } + fn broadcast_pauli_product( + &mut self, + instruction: &Instruction, + operation: impl FnMut(&mut Self, StimQubitId, bool), + ) { + self.unsupported_args(instruction); + self.for_each_pauli_product(instruction, operation); + } + + fn broadcast_pauli_product_measure( + &mut self, + instruction: &Instruction, + mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, + ) { + let Some(readout_noise) = self.expect_readout_noise(instruction) else { + return; + }; + self.for_each_pauli_product(instruction, |s, q, negated| { + let result_id = measure(s, q, negated); + s.op_readout_noise(readout_noise, result_id); + }); + } + + fn broadcast_rotation( + &mut self, + instruction: &Instruction, + mut operation: impl FnMut(&mut Self, Radians, StimQubitId), + ) { + let Some(angle) = self.expect_angle(instruction) else { + return; + }; + self.for_each_qubit(instruction, |s, q| operation(s, angle, q)); + } + + fn broadcast_pair_rotation( + &mut self, + instruction: &Instruction, + mut operation: impl FnMut(&mut Self, Radians, StimQubitId, StimQubitId), + ) { + let Some(angle) = self.expect_angle(instruction) else { + return; + }; + self.for_each_pair(instruction, |s, q0, q1| operation(s, angle, q0, q1)); + } + fn broadcast_controlled( &mut self, instruction: &Instruction, @@ -1846,6 +1803,49 @@ impl<'noise> Compiler<'noise> { self.writer.write_classical_control(pauli, result_id, qubit); } + fn accumulate_correlated_noise(&mut self, instruction: &Instruction) { + let Some(probability) = self.expect_arg(instruction) else { + return; + }; + let mut terms = Vec::with_capacity(instruction.targets.len()); + + for target in &instruction.targets { + let Some((fault, qubit)) = self.expect_fault_char(instruction, target) else { + continue; + }; + + terms.push((fault, qubit)); + } + + let row = CorrelatedRow { + probability, + terms, + span: instruction.span, + }; + + self.noise_accumulator.push_correlated_row(row); + } + + fn continue_correlated_noise(&mut self, instruction: &Instruction) { + if self.noise_accumulator.current_correlated_group.is_none() { + self.push_error(Error::OrphanedElseCorrelatedError { + span: instruction.span, + }); + return; + } + self.accumulate_correlated_noise(instruction); + } + + fn finish_correlated_noise(&mut self) { + if self.noise_accumulator.current_correlated_group.is_none() { + return; + } + match self.noise_accumulator.flush_correlated_group() { + Ok((noise_table, qubits)) => self.op_noise(noise_table, &qubits), + Err(error) => self.push_error(error), + } + } + /// Converts a Pauli product to a canonical form: one factor per qubit, sorted by /// qubit index, with identity factors removed. Rejects anti-Hermitian products and /// represents an overall phase of -1 as a negation. @@ -1973,6 +1973,17 @@ impl<'noise> Compiler<'noise> { self.writer.write_qis_call(intrinsic, &[q0, q1, q2]); } + fn op_rotation(&mut self, intrinsic: &str, angle: Radians, qubit: StimQubitId) { + let qubit = self.id_map.allocate_qubit(qubit); + self.writer.write_rotation_call(intrinsic, angle, &[qubit]); + } + + fn op_rotation_2(&mut self, intrinsic: &str, angle: Radians, q0: StimQubitId, q1: StimQubitId) { + let q0 = self.id_map.allocate_qubit(q0); + let q1 = self.id_map.allocate_qubit(q1); + self.writer.write_rotation_call(intrinsic, angle, &[q0, q1]); + } + fn op_adj(&mut self, intrinsic: &str, qubit: StimQubitId) { let q = self.id_map.allocate_qubit(qubit); self.writer.write_qis_adj_call(intrinsic, &[q]); @@ -2024,17 +2035,6 @@ impl<'noise> Compiler<'noise> { } } - fn op_rotation(&mut self, intrinsic: &str, angle: Radians, qubit: StimQubitId) { - let qubit = self.id_map.allocate_qubit(qubit); - self.writer.write_rotation_call(intrinsic, angle, &[qubit]); - } - - fn op_rotation_2(&mut self, intrinsic: &str, angle: Radians, q0: StimQubitId, q1: StimQubitId) { - let q0 = self.id_map.allocate_qubit(q0); - let q1 = self.id_map.allocate_qubit(q1); - self.writer.write_rotation_call(intrinsic, angle, &[q0, q1]); - } - fn build_noise_table( &mut self, num_qubits: u32, From 16adf0809d07d8d9e0757ae7d5160d5a94401fcf Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 25 Aug 2026 16:45:53 -0700 Subject: [PATCH 03/18] add unit tests for remaining non_clifford_gates --- .../src/qir/tests/non_clifford_gates.rs | 1390 ++++++++++++++++- 1 file changed, 1340 insertions(+), 50 deletions(-) diff --git a/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs b/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs index f9674edf220..9a974ebaca2 100644 --- a/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs +++ b/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs @@ -147,20 +147,21 @@ fn t_gate_with_pauli_target_yields_error() { } #[test] -fn r_x_yields_expected_qir() { +fn tpp_single_z_yields_expected_qir() { + // same as T 0 check( - "R_X(0.25) 0", + "TPP Z0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__rx__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__qis__rx__body(double, ptr) declare void @__quantum__rt__result_record_output(ptr, ptr) declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) declare void @__quantum__rt__initialize(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } @@ -183,21 +184,24 @@ fn r_x_yields_expected_qir() { } #[test] -fn r_y_with_negative_angle_yields_expected_qir() { +fn tpp_single_x_yields_expected_qir() { check( - "R_Y(-0.25) 0", + "TPP X0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__ry__body(double -0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__qis__ry__body(double, ptr) declare void @__quantum__rt__result_record_output(ptr, ptr) declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } attributes #1 = { "irreversible" } @@ -219,21 +223,28 @@ fn r_y_with_negative_angle_yields_expected_qir() { } #[test] -fn r_z_with_large_angle_yields_expected_qir() { +fn tpp_single_y_yields_expected_qir() { check( - "R_Z(123.432) 0", + "TPP Y0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__rz__body(double 387.77306441789534, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__s__body(ptr) declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__t__body(ptr) declare void @__quantum__rt__initialize(ptr) - declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__qis__h__body(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } attributes #1 = { "irreversible" } @@ -255,23 +266,72 @@ fn r_z_with_large_angle_yields_expected_qir() { } #[test] -fn r_x_broadcasts_over_targets() { +fn tpp_dag_single_z_yields_expected_qir() { + // same as T_DAG 0 check( - "R_X(0.125) 0 1 2", + "TPP_DAG Z0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 0 to ptr)) - call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 1 to ptr)) - call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 2 to ptr)) + call void @__quantum__qis__t__adj(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__qis__rx__body(double, ptr) declare void @__quantum__rt__result_record_output(ptr, ptr) declare void @__quantum__rt__array_record_output(i64, ptr) declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__t__adj(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_three_factor_product_yields_expected_qir() { + check( + "TPP X0*Y1*Z2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__s__body(ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__h__body(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__t__body(ptr) + declare void @__quantum__rt__initialize(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } attributes #1 = { "irreversible" } @@ -293,70 +353,1300 @@ fn r_x_broadcasts_over_targets() { } #[test] -fn r_x_without_argument_yields_error() { +fn tpp_negated_product_applies_inverse() { check( - "R_X 0", + "TPP !Z0", &expect![[r#" - Qdk.Stim.Compiler.MissingArg + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__t__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } - x missing argument in instruction: R_X - ,---- - 1 | R_X 0 - : ^^^^^ - `---- + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__t__adj(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} "#]], ); } #[test] -fn r_x_with_two_arguments_yields_error() { +fn tpp_dag_negated_product_applies_inverse() { check( - "R_X(0.25, 0.5) 0", + "TPP_DAG !Z0", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } - x instruction R_X requires 1 arguments, but found 2 + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_negation_on_later_factor_negates_whole_product() { + check( + "TPP X0*!Z1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__qis__t__adj(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_double_negation_cancels() { + check( + "TPP !X0*!Z1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_identity_products_are_noops() { + let source = indoc! {" + TPP X0*X0 !Y1*Y1 + TPP_DAG Z2*Z2 !X3*X3 + "}; + check( + source, + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="0" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_anti_hermitian_product_yields_error() { + check( + "TPP X0*Y0", + &expect![[r#" + Qdk.Stim.Compiler.AntiHermitianPauliProduct + + x Pauli product must be Hermitian ,---- - 1 | R_X(0.25, 0.5) 0 - : ^^^^^^^^^^^^^^^^ + 1 | TPP X0*Y0 + : ^^^^^ `---- "#]], ); } #[test] -fn r_x_with_negated_target_yields_error() { +fn tpp_with_argument_yields_error() { check( - "R_X(0.25) !0", + "TPP(0.5) Z0", &expect![[r#" - Qdk.Stim.Compiler.NegatedTarget + Qdk.Stim.Compiler.UnsupportedArgument - x target cannot be negated in instruction: R_X + x unsupported argument in instruction: TPP ,---- - 1 | R_X(0.25) !0 - : ^^ + 1 | TPP(0.5) Z0 + : ^^^^^^^^^^^ `---- "#]], ); } #[test] -fn r_x_with_angle_that_overflows_radians_yields_error() { +fn tpp_with_qubit_target_yields_error() { check( - "R_X(1e308) 0", + "TPP 0", &expect![[r#" - Qdk.Stim.Compiler.InvalidAngle + Qdk.Stim.Compiler.UnsupportedTarget - x angle for R_X must be finite and representable in radians; found - | 10000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 0000000000000 half turns + x unsupported target in instruction: TPP ,---- - 1 | R_X(1e308) 0 - : ^^^^^^^^^^^^ + 1 | TPP 0 + : ^ `---- "#]], ); } + +#[test] +fn ch_gate_yields_expected_qir() { + check( + "CH 0 1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ry__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double -0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccz_gate_yields_expected_qir() { + check( + "CCZ 0 1 2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ccx__body(ptr, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccx_gate_yields_expected_qir() { + check( + "CCX 0 1 2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 2 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ccx__body(ptr, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccz_gate_broadcasts_over_triples() { + check( + "CCZ 0 1 2 3 4 5", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 3 to ptr)) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 4 to ptr), ptr inttoptr (i64 5 to ptr), ptr inttoptr (i64 3 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 3 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ccx__body(ptr, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="6" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccx_gate_with_one_target_yields_error() { + check( + "CCX 0", + &expect![[r#" + Qdk.Stim.Compiler.TargetCountNotMultipleOfThree + + x instruction CCX requires a multiple of three targets + ,---- + 1 | CCX 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_two_targets_yields_error() { + check( + "CCX 0 1", + &expect![[r#" + Qdk.Stim.Compiler.TargetCountNotMultipleOfThree + + x instruction CCX requires a multiple of three targets + ,---- + 1 | CCX 0 1 + : ^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_four_targets_yields_error() { + check( + "CCX 0 1 2 3", + &expect![[r#" + Qdk.Stim.Compiler.TargetCountNotMultipleOfThree + + x instruction CCX requires a multiple of three targets + ,---- + 1 | CCX 0 1 2 3 + : ^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_argument_yields_error() { + check( + "CCX(0.5) 0 1 2", + &expect![[r#" + Qdk.Stim.Compiler.UnsupportedArgument + + x unsupported argument in instruction: CCX + ,---- + 1 | CCX(0.5) 0 1 2 + : ^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_negated_target_yields_error() { + check( + "CCX 0 1 !2", + &expect![[r#" + Qdk.Stim.Compiler.NegatedTarget + + x target cannot be negated in instruction: CCX + ,---- + 1 | CCX 0 1 !2 + : ^^ + `---- + "#]], + ); +} + +#[test] +fn ccz_gate_with_measurement_record_target_yields_error() { + let source = indoc! {" + M 0 + CCZ rec[-1] 1 2 + "}; + check( + source, + &expect![[r#" + Qdk.Stim.Compiler.UnsupportedTarget + + x unsupported target in instruction: CCZ + ,-[2:5] + 1 | M 0 + 2 | CCZ rec[-1] 1 2 + : ^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_x_yields_expected_qir() { + check( + "R_X(0.25) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rx__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rx__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_y_yields_expected_qir() { + check( + "R_Y(-0.375) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ry__body(double -1.1780972450961724, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_z_yields_expected_qir() { + check( + "R_Z(123.432) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 387.77306441789534, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_x_broadcasts_over_targets() { + check( + "R_X(0.125) 0 1 2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 2 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rx__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_x_without_argument_yields_error() { + check( + "R_X 0", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: R_X + ,---- + 1 | R_X 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_x_with_two_arguments_yields_error() { + check( + "R_X(0.25, 0.5) 0", + &expect![[r#" + Qdk.Stim.Compiler.WrongArgCount + + x instruction R_X requires 1 arguments, but found 2 + ,---- + 1 | R_X(0.25, 0.5) 0 + : ^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_x_with_negated_target_yields_error() { + check( + "R_X(0.25) !0", + &expect![[r#" + Qdk.Stim.Compiler.NegatedTarget + + x target cannot be negated in instruction: R_X + ,---- + 1 | R_X(0.25) !0 + : ^^ + `---- + "#]], + ); +} + +#[test] +fn u3_yields_expected_qir() { + check( + "U3(0.1, 0.2, 0.3) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.9424777960769379, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double 0.3141592653589793, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.6283185307179586, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn u_alias_yields_expected_qir() { + check( + "U(0.1, 0.2, 0.3) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.9424777960769379, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double 0.3141592653589793, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.6283185307179586, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn u3_without_arguments_yields_error() { + check( + "U3 0", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: U3 + ,---- + 1 | U3 0 + : ^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_one_argument_yields_error() { + check( + "U3(0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.WrongArgCount + + x instruction U3 requires 3 arguments, but found 1 + ,---- + 1 | U3(0.1) 0 + : ^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_two_arguments_yields_error() { + check( + "U3(0.1, 0.2) 0", + &expect![[r#" + Qdk.Stim.Compiler.WrongArgCount + + x instruction U3 requires 3 arguments, but found 2 + ,---- + 1 | U3(0.1, 0.2) 0 + : ^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_four_arguments_yields_error() { + check( + "U3(0.1, 0.2, 0.3, 0.4) 0", + &expect![[r#" + Qdk.Stim.Compiler.WrongArgCount + + x instruction U3 requires 3 arguments, but found 4 + ,---- + 1 | U3(0.1, 0.2, 0.3, 0.4) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_multiple_angles_that_overflow_radians_yields_errors() { + check( + "U3(1e308, 0.25, -1e308) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidAngle + + x angle for U3 must be finite and representable in radians; found + | 10000000000000000000000000000000000000000000000000000000000000000000000000 + | 00000000000000000000000000000000000000000000000000000000000000000000000000 + | 00000000000000000000000000000000000000000000000000000000000000000000000000 + | 00000000000000000000000000000000000000000000000000000000000000000000000000 + | 0000000000000 half turns + ,---- + 1 | U3(1e308, 0.25, -1e308) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + + Qdk.Stim.Compiler.InvalidAngle + + x angle for U3 must be finite and representable in radians; found + | -1000000000000000000000000000000000000000000000000000000000000000000000000 + | 00000000000000000000000000000000000000000000000000000000000000000000000000 + | 00000000000000000000000000000000000000000000000000000000000000000000000000 + | 00000000000000000000000000000000000000000000000000000000000000000000000000 + | 00000000000000 half turns + ,---- + 1 | U3(1e308, 0.25, -1e308) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_xx_yields_expected_qir() { + check( + "R_XX(0.25) 0 1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rxx__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rxx__body(double, ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_yy_yields_expected_qir() { + check( + "R_YY(-0.6) 0 1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ryy__body(double -1.8849555921538759, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__ryy__body(double, ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_zz_yields_expected_qir() { + check( + "R_ZZ(0.25) 0 1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rzz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rzz__body(double, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_zz_broadcasts_over_pairs() { + check( + "R_ZZ(0.25) 0 1 2 3", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rzz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__rzz__body(double 0.7853981633974483, ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 3 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rzz__body(double, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="4" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_xx_with_odd_target_count_yields_error() { + check( + "R_XX(0.25) 0 1 2", + &expect![[r#" + Qdk.Stim.Compiler.OddTargetCount + + x instruction R_XX requires an even number of targets + ,---- + 1 | R_XX(0.25) 0 1 2 + : ^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_xx_without_argument_yields_error() { + check( + "R_XX 0 1", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: R_XX + ,---- + 1 | R_XX 0 1 + : ^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_xx_with_two_arguments_yields_error() { + check( + "R_XX(0.25, 0.5) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.WrongArgCount + + x instruction R_XX requires 1 arguments, but found 2 + ,---- + 1 | R_XX(0.25, 0.5) 0 1 + : ^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_pauli_single_z_yields_expected_qir() { + // same as R_Z(0.25) 0 + check( + "R_PAULI(0.25) Z0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_single_x_yields_expected_qir() { + check( + "R_PAULI(0.25) X0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__h__body(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_single_y_yields_expected_qir() { + check( + "R_PAULI(0.25) Y0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__s__body(ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_mixed_basis_product_yields_expected_qir() { + check( + "R_PAULI(0.25) X0*Y1*Z2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__qis__s__body(ptr) + declare void @__quantum__qis__h__body(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_negated_product_negates_angle() { + check( + "R_PAULI(0.25) !Z0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double -0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_without_argument_yields_error() { + check( + "R_PAULI X0", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: R_PAULI + ,---- + 1 | R_PAULI X0 + : ^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_pauli_with_two_arguments_yields_error() { + check( + "R_PAULI(0.25, 0.5) X0", + &expect![[r#" + Qdk.Stim.Compiler.WrongArgCount + + x instruction R_PAULI requires 1 arguments, but found 2 + ,---- + 1 | R_PAULI(0.25, 0.5) X0 + : ^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} From d0eb84a1941664f55f1e91516516752d2f2c0c2b Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 25 Aug 2026 16:47:23 -0700 Subject: [PATCH 04/18] lexer recognizes doubles with rad suffixes --- source/compiler/stim_compiler/src/lex.rs | 172 ++++++++++++------ .../stim_compiler/src/lex/tests/number.rs | 86 +++++++++ 2 files changed, 202 insertions(+), 56 deletions(-) diff --git a/source/compiler/stim_compiler/src/lex.rs b/source/compiler/stim_compiler/src/lex.rs index 02219b0750a..c994900083e 100644 --- a/source/compiler/stim_compiler/src/lex.rs +++ b/source/compiler/stim_compiler/src/lex.rs @@ -61,20 +61,20 @@ impl Display for Token { #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum TokenKind { - Newline, // \n - Uint, // unsigned integers - Double, // floating-point numbers - InstructionName, // H, X, CNOT, etc. - Pauli, // X1, Y2, Z3, etc. - Loss, // L1, L2, L3, etc. - Rec, // rec[- ...] - Sweep, // sweep[...] - Tag, // "[...]" - Open(Delim), // ( { - Close(Delim), // ) } - Star, // * - Bang, // ! - Comma, // , + Newline, // \n + Uint, // unsigned integers + Double(DoubleKind), // floating-point numbers, can be radians or not + InstructionName, // H, X, CNOT, etc. + Pauli, // X1, Y2, Z3, etc. + Loss, // L1, L2, L3, etc. + Rec, // rec[- ...] + Sweep, // sweep[...] + Tag, // "[...]" + Open(Delim), // ( { + Close(Delim), // ) } + Star, // * + Bang, // ! + Comma, // , } impl Display for TokenKind { @@ -82,7 +82,7 @@ impl Display for TokenKind { match self { TokenKind::Newline => f.write_str("newline"), TokenKind::Uint => f.write_str("uint"), - TokenKind::Double => f.write_str("double"), + TokenKind::Double(_) => f.write_str("double"), TokenKind::InstructionName => f.write_str("instruction_name"), TokenKind::Pauli => f.write_str("pauli"), TokenKind::Loss => f.write_str("loss"), @@ -98,6 +98,21 @@ impl Display for TokenKind { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] +pub enum DoubleKind { + Default, // for angles, interpret as half turns (pi radians) + Radians, +} + +impl Display for DoubleKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + DoubleKind::Default => f.write_str("default"), + DoubleKind::Radians => f.write_str("radians"), + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum Delim { Paren, @@ -159,60 +174,105 @@ impl<'a> Lexer<'a> { true } - fn scan_number(&mut self, lo: u32, signed: bool) -> Result { - // Lexes a number: an optional sign, an integer part, an optional - // fractional part, and an optional exponent. + fn eat_str(&mut self, expected: &str) -> bool { + let pos = self.pos() as usize; + if !self.input[pos..].starts_with(expected) { + return false; + } + + for _ in expected.chars() { + let _ = self.chars.next(); + } + true + } + + fn require_digits(&mut self, error: Error) -> Result<(), Error> { + if self.eat_one_or_more_digits() { + Ok(()) + } else { + Err(error) + } + } + + /// Scans an optional "rad" suffix, which indicates that a number is in radians. + /// If the suffix is present, it must be followed by a non-alphanumeric character or the end of the input. + /// "1", "2.5", "-6" + fn scan_rad_suffix(&mut self) -> Result { + if !self.eat_str("rad") { + return Ok(false); + } + + let lo = self.pos(); + if self + .chars + .next_if(|(_, c)| c.is_alphanumeric() || *c == '_') + .is_some() + { + return Err(Error::UnrecognizedCharacter { + span: Span { lo, hi: self.pos() }, + }); + } + Ok(true) + } + + /// Scans an optional exponent: 'e'/'E', an optional sign, then one or more digits. + /// "1", "2.5", "6" + /// A bare "1e" or "1e-" (no exponent digits) is an error. + fn scan_exponent(&mut self, lo: u32) -> Result { + if self + .chars + .next_if(|(_, c)| matches!(c, 'e' | 'E')) + .is_none() + { + return Ok(false); + } - let mut is_double = false; + self.chars.next_if(|(_, c)| matches!(c, '+' | '-')); + let span = Span { lo, hi: self.pos() }; + self.require_digits(Error::MissingExponentDigits { span })?; + Ok(true) + } + + /// Scans an optional fractional part: a '.' followed by one or more digits. + /// "3<.14>", "0<.5>" + /// A '.' with no digits after it ("3.") is an error. + fn scan_fraction(&mut self, lo: u32) -> Result { + if self.chars.next_if(|(_, c)| *c == '.').is_none() { + return Ok(false); + } + + let span = Span { lo, hi: self.pos() }; + self.require_digits(Error::MissingFractionalDigits { span })?; + Ok(true) + } + + /// Scans the integer part of a number, which may be signed or unsigned. + fn scan_integer_part(&mut self, lo: u32, signed: bool) -> Result<(), Error> { if signed { // The leading sign was already consumed by the caller: // "<+>1", "<->42", "<+>3.5e-2" // This block consumes the integer digits: "+<1>", "-<42>" - if !self.eat_one_or_more_digits() { - return Err(Error::MissingDigitsAfterSign { - span: Span { lo, hi: self.pos() }, - }); - } - is_double = true; // A signed number is always a double. + let span = Span { lo, hi: self.pos() }; + self.require_digits(Error::MissingDigitsAfterSign { span }) } else { // The first digit was already consumed by the caller: // "<4>2", "<3>.14" // This block consumes the remaining integer digits: "4<2>" self.eat_while(|c| c.is_ascii_digit()); + Ok(()) } + } - if self.chars.next_if(|(_, c)| *c == '.').is_some() { - // Optional fractional part: a '.' followed by one or more digits. - // "3<.14>", "0<.5>" - // A '.' with no digits after it ("3.") is an error. - if !self.eat_one_or_more_digits() { - return Err(Error::MissingFractionalDigits { - span: Span { lo, hi: self.pos() }, - }); - } - is_double = true; - } - if self - .chars - .next_if(|(_, c)| *c == 'e' || *c == 'E') - .is_some() - { - // Optional exponent: 'e'/'E', an optional sign, then one or more digits. - // "1", "2.5", "6" - // A bare "1e" or "1e-" (no exponent digits) is an error. - self.chars.next_if(|(_, c)| *c == '+' || *c == '-'); - if !self.eat_one_or_more_digits() { - return Err(Error::MissingExponentDigits { - span: Span { lo, hi: self.pos() }, - }); - } - is_double = true; - } + fn scan_number(&mut self, lo: u32, signed: bool) -> Result { + self.scan_integer_part(lo, signed)?; + let has_fraction = self.scan_fraction(lo)?; + let has_exponent = self.scan_exponent(lo)?; + let has_rad_suffix = self.scan_rad_suffix()?; - // No '.' and no exponent => an unsigned integer ("42" => Uint); - // a sign, '.', or exponent makes it a Double ("-42", "3.14", "1e9"). - Ok(if is_double { - TokenKind::Double + Ok(if has_rad_suffix { + TokenKind::Double(DoubleKind::Radians) + } else if signed || has_fraction || has_exponent { + TokenKind::Double(DoubleKind::Default) } else { TokenKind::Uint }) diff --git a/source/compiler/stim_compiler/src/lex/tests/number.rs b/source/compiler/stim_compiler/src/lex/tests/number.rs index be1be612a8f..7ed3f8c3c0a 100644 --- a/source/compiler/stim_compiler/src/lex/tests/number.rs +++ b/source/compiler/stim_compiler/src/lex/tests/number.rs @@ -320,3 +320,89 @@ fn double_sign_recovers_to_a_double() { double(+1) [1-3]"#]], ); } + +#[test] +fn unsigned_integer_with_rad_suffix_lexes_as_double() { + check("1rad", &expect!["double(1rad) [0-4]"]); + check("12rad", &expect!["double(12rad) [0-5]"]); + check("123rad", &expect!["double(123rad) [0-6]"]); +} + +#[test] +fn signed_integer_with_rad_suffix_lexes_as_double() { + check("+1rad", &expect!["double(+1rad) [0-5]"]); + check("-1rad", &expect!["double(-1rad) [0-5]"]); +} + +#[test] +fn double_radians() { + check("1.0rad", &expect!["double(1.0rad) [0-6]"]); + check("3.14rad", &expect!["double(3.14rad) [0-7]"]); + check("2e-5rad", &expect!["double(2e-5rad) [0-7]"]); + check("-0.01rad", &expect!["double(-0.01rad) [0-8]"]); +} + +#[test] +fn rad_suffix_preserves_delimiters() { + check( + "1rad,2rad)", + &expect![[r#" + double(1rad) [0-4] + comma(,) [4-5] + double(2rad) [5-9] + close(paren)()) [9-10]"#]], + ); +} + +#[test] +fn rad_suffix_followed_by_invalid_character_yields_error() { + check( + "1radX", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1radX + : ^ + `---- + "#]], + ); + check( + "1radx", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1radx + : ^ + `---- + "#]], + ); + check( + "1rad0", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1rad0 + : ^ + `---- + "#]], + ); + check( + "1rad_foo", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1rad_foo + : ^ + `---- + + instruction_name(foo) [5-8]"#]], + ); +} From 5e942840bc5e805422049b50fa27c15f7a14b773 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 26 Aug 2026 11:14:49 -0700 Subject: [PATCH 05/18] rename DoubleKind to DoubleUnit --- source/compiler/stim_compiler/src/lex.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/source/compiler/stim_compiler/src/lex.rs b/source/compiler/stim_compiler/src/lex.rs index c994900083e..e6a43312ed4 100644 --- a/source/compiler/stim_compiler/src/lex.rs +++ b/source/compiler/stim_compiler/src/lex.rs @@ -63,7 +63,7 @@ impl Display for Token { pub enum TokenKind { Newline, // \n Uint, // unsigned integers - Double(DoubleKind), // floating-point numbers, can be radians or not + Double(DoubleUnit), // floating-point numbers, can be radians or not InstructionName, // H, X, CNOT, etc. Pauli, // X1, Y2, Z3, etc. Loss, // L1, L2, L3, etc. @@ -99,16 +99,16 @@ impl Display for TokenKind { } #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] -pub enum DoubleKind { +pub enum DoubleUnit { Default, // for angles, interpret as half turns (pi radians) Radians, } -impl Display for DoubleKind { +impl Display for DoubleUnit { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - DoubleKind::Default => f.write_str("default"), - DoubleKind::Radians => f.write_str("radians"), + DoubleUnit::Default => f.write_str("default"), + DoubleUnit::Radians => f.write_str("radians"), } } } @@ -270,9 +270,9 @@ impl<'a> Lexer<'a> { let has_rad_suffix = self.scan_rad_suffix()?; Ok(if has_rad_suffix { - TokenKind::Double(DoubleKind::Radians) + TokenKind::Double(DoubleUnit::Radians) } else if signed || has_fraction || has_exponent { - TokenKind::Double(DoubleKind::Default) + TokenKind::Double(DoubleUnit::Default) } else { TokenKind::Uint }) From db640e47db4cd414731b9792b0694561b8a9f23d Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 26 Aug 2026 14:41:07 -0700 Subject: [PATCH 06/18] parser recognizes doubles with radian units --- source/compiler/stim_compiler/src/parser.rs | 124 +++++++---- .../src/parser/tests/arguments.rs | 198 ++++++++++++++++-- .../src/parser/tests/instruction_shapes.rs | 30 +-- .../stim_compiler/src/parser/tests/spans.rs | 72 ++++++- .../stim_compiler/src/parser/tests/tags.rs | 22 +- .../stim_compiler/src/parser/tests/targets.rs | 22 +- 6 files changed, 362 insertions(+), 106 deletions(-) diff --git a/source/compiler/stim_compiler/src/parser.rs b/source/compiler/stim_compiler/src/parser.rs index f309a1908e8..d6a658d9a40 100644 --- a/source/compiler/stim_compiler/src/parser.rs +++ b/source/compiler/stim_compiler/src/parser.rs @@ -7,7 +7,7 @@ mod tests; use crate::lex::{ self, Delim::{Brace, Paren}, - Lexer, Token, + DoubleUnit, Lexer, Token, TokenKind::{self}, }; use miette::Diagnostic; @@ -80,7 +80,7 @@ pub struct Instruction { pub span: Span, pub name: String, pub tag: Option, - pub args: Vec, + pub args: Vec, pub targets: Vec, } @@ -94,6 +94,34 @@ impl Display for Instruction { } } +#[derive(Debug, Clone, Copy)] +pub struct Arg { + pub span: Span, + pub value: ArgValue, +} + +impl Display for Arg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln_header_with_span(f, "Arg", self.span)?; + writeln_field(f, "value", &self.value) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ArgValue { + Default(f64), + Radians(f64), +} + +impl Display for ArgValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ArgValue::Default(value) => write!(f, "{value}"), + ArgValue::Radians(value) => write!(f, "{value} rad"), + } + } +} + #[derive(Debug)] pub struct Target { pub span: Span, @@ -250,6 +278,12 @@ pub enum Error { #[label] span: Span, }, + #[error("floating-point literal is too large to fit in a 64-bit float")] + #[diagnostic(code("Qdk.Stim.Parser.FloatTooLarge"))] + FloatTooLarge { + #[label] + span: Span, + }, #[error("measurement record offset cannot be zero; the most recent measurement is rec[-1]")] #[diagnostic(code("Qdk.Stim.Parser.ZeroMeasurementRecord"))] ZeroMeasurementRecord { @@ -369,26 +403,6 @@ impl<'a> Parser<'a> { } } - fn expect_number(&mut self) -> Option { - match self.next() { - Some(token) if token.kind == TokenKind::Uint || token.kind == TokenKind::Double => { - Some(token) - } - Some(token) => { - self.emit_error(Error::Expected { - expected: "number", - found: token.kind, - span: token.span, - }); - None - } - None => { - self.emit_eof_error(); - None - } - } - } - fn expect_line_end(&mut self) -> Option<()> { match self.peek() { None => Some(()), // End of file @@ -493,17 +507,14 @@ impl<'a> Parser<'a> { fn parse_instruction(&mut self) -> Option { let name_token = self.expect_token(TokenKind::InstructionName)?; let lo = name_token.span.lo; - let name = self.extract_string(name_token, None); + let name = self.extract_string(name_token.span); let tag_token = self.next_if(|t| t.kind == TokenKind::Tag); let tag: Option = tag_token.map(|tag_token| { - self.extract_string( - tag_token, - Some(Span { - lo: tag_token.span.lo + 1, - hi: tag_token.span.hi - 1, - }), - ) + self.extract_string(Span { + lo: tag_token.span.lo + 1, + hi: tag_token.span.hi - 1, + }) }); let mut args = Vec::new(); @@ -520,8 +531,7 @@ impl<'a> Parser<'a> { .peek() .is_some_and(|t| t.kind != TokenKind::Close(Paren)) { - let arg = self.expect_number()?; - args.push(self.extract_double(arg, None)); + args.push(self.parse_arg()?); } // Each subsequent arg must be preceded by a comma while self @@ -529,8 +539,7 @@ impl<'a> Parser<'a> { .is_some_and(|t| t.kind != TokenKind::Close(Paren)) { self.expect_token(TokenKind::Comma)?; - let arg = self.expect_number()?; - args.push(self.extract_double(arg, None)); + args.push(self.parse_arg()?); } paren_hi = Some(self.expect_token(TokenKind::Close(Paren))?.span.hi); } @@ -560,6 +569,36 @@ impl<'a> Parser<'a> { }) } + fn parse_arg(&mut self) -> Option { + let token = self.expect_any()?; + + let value = match token.kind { + TokenKind::Uint | TokenKind::Double(DoubleUnit::Default) => { + ArgValue::Default(self.extract_double(token.span)?) + } + TokenKind::Double(DoubleUnit::Radians) => { + let value_span = Span { + lo: token.span.lo, + hi: token.span.hi - 3, // strip "rad" suffix + }; + ArgValue::Radians(self.extract_double(value_span)?) + } + found => { + self.emit_error(Error::Expected { + expected: "number", + found, + span: token.span, + }); + return None; + } + }; + + Some(Arg { + span: token.span, + value, + }) + } + fn parse_target(&mut self) -> Option { let negated_token = self.next_if(|t| t.kind == TokenKind::Bang); let negated = negated_token.is_some(); @@ -722,13 +761,20 @@ impl<'a> Parser<'a> { } } - fn extract_double(&self, token: Token, span: Option) -> f64 { - self.extract_string(token, span) + fn extract_double(&mut self, value_span: Span) -> Option { + let value = self + .slice_input(value_span) .parse::() - .unwrap_or_else(|_| unreachable!("lexer guarantees a valid double literal")) + .unwrap_or_else(|_| unreachable!("lexer guarantees a valid double literal")); + + if !value.is_finite() { + self.emit_error(Error::FloatTooLarge { span: value_span }); + return None; + } + Some(value) } - fn extract_string(&self, token: Token, span: Option) -> String { - self.slice_input(span.unwrap_or(token.span)).to_string() + fn extract_string(&self, source_span: Span) -> String { + self.slice_input(source_span).to_string() } } diff --git a/source/compiler/stim_compiler/src/parser/tests/arguments.rs b/source/compiler/stim_compiler/src/parser/tests/arguments.rs index 6fb5ecb99d3..54e0e6bd76c 100644 --- a/source/compiler/stim_compiler/src/parser/tests/arguments.rs +++ b/source/compiler/stim_compiler/src/parser/tests/arguments.rs @@ -9,57 +9,209 @@ fn single_arg() { check( "DEPOLARIZE1(0.001) 0", &expect![[r#" - Circuit [0-20]: + Circuit [0-20]: + items: + Instruction [0-20]: + name: DEPOLARIZE1 + tag: + args: + Arg [12-17]: + value: 0.001 + + targets: + Target [19-20]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn multiple_comma_separated_args() { + check( + "PAULI_CHANNEL_1(0.01, 0.02, 0.03) 0", + &expect![[r#" + Circuit [0-35]: + items: + Instruction [0-35]: + name: PAULI_CHANNEL_1 + tag: + args: + Arg [16-20]: + value: 0.01 + + Arg [22-26]: + value: 0.02 + + Arg [28-32]: + value: 0.03 + + targets: + Target [34-35]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn scientific_notation_arg() { + check( + "X_ERROR(1e-3) 0", + &expect![[r#" + Circuit [0-15]: + items: + Instruction [0-15]: + name: X_ERROR + tag: + args: + Arg [8-12]: + value: 0.001 + + targets: + Target [14-15]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn radians_args() { + check( + "R_X(1rad) 0", + &expect![[r#" + Circuit [0-11]: items: - Instruction [0-20]: - name: DEPOLARIZE1 + Instruction [0-11]: + name: R_X tag: args: - 0.001 + Arg [4-8]: + value: 1 rad + targets: - Target [19-20]: + Target [10-11]: kind: Qubit(0)"#]], ); -} + check( + "R_Y(-0.5rad) 0", + &expect![[r#" + Circuit [0-14]: + items: + Instruction [0-14]: + name: R_Y + tag: + args: + Arg [4-11]: + value: -0.5 rad -#[test] -fn multiple_comma_separated_args() { + targets: + Target [13-14]: + kind: Qubit(0)"#]], + ); check( - "PAULI_CHANNEL_1(0.01, 0.02, 0.03) 0", + "R_Z(+2.5e-3rad) 0", &expect![[r#" - Circuit [0-35]: + Circuit [0-17]: items: - Instruction [0-35]: - name: PAULI_CHANNEL_1 + Instruction [0-17]: + name: R_Z tag: args: - 0.01 - 0.02 - 0.03 + Arg [4-14]: + value: 0.0025 rad + targets: - Target [34-35]: + Target [16-17]: kind: Qubit(0)"#]], ); } #[test] -fn scientific_notation_arg() { +fn mixed_unit_args() { check( - "X_ERROR(1e-3) 0", + "U3(0.1, -0.2rad, 3e-1rad) 0", &expect![[r#" - Circuit [0-15]: + Circuit [0-27]: items: - Instruction [0-15]: - name: X_ERROR + Instruction [0-27]: + name: U3 tag: args: - 0.001 + Arg [3-6]: + value: 0.1 + + Arg [8-15]: + value: -0.2 rad + + Arg [17-24]: + value: 0.3 rad + targets: - Target [14-15]: + Target [26-27]: kind: Qubit(0)"#]], ); } +#[test] +fn unitless_float_too_large_is_error() { + check( + "X_ERROR(1e999) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | X_ERROR(1e999) 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn negative_unitless_float_too_large_is_error() { + check( + "X_ERROR(-1e999) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | X_ERROR(-1e999) 0 + : ^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn radians_float_too_large_is_error() { + check( + "R_X(1e999rad) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | R_X(1e999rad) 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn negative_radians_float_too_large_is_error() { + check( + "R_X(-1e999rad) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | R_X(-1e999rad) 0 + : ^^^^^^ + `---- + "#]], + ); +} + #[test] fn trailing_comma_is_error() { check( diff --git a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs index 4ccaa14ca11..3dba877ffe0 100644 --- a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs +++ b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs @@ -52,7 +52,9 @@ fn args_no_targets() { name: X_ERROR tag: args: - 0.1 + Arg [8-11]: + value: 0.1 + targets: "#]], ); } @@ -62,18 +64,20 @@ fn args_with_targets() { check( "X_ERROR(0.1) 0 1", &expect![[r#" - Circuit [0-16]: - items: - Instruction [0-16]: - name: X_ERROR - tag: - args: - 0.1 - targets: - Target [13-14]: - kind: Qubit(0) - Target [15-16]: - kind: Qubit(1)"#]], + Circuit [0-16]: + items: + Instruction [0-16]: + name: X_ERROR + tag: + args: + Arg [8-11]: + value: 0.1 + + targets: + Target [13-14]: + kind: Qubit(0) + Target [15-16]: + kind: Qubit(1)"#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/spans.rs b/source/compiler/stim_compiler/src/parser/tests/spans.rs index 853185aeecb..6774e61fee8 100644 --- a/source/compiler/stim_compiler/src/parser/tests/spans.rs +++ b/source/compiler/stim_compiler/src/parser/tests/spans.rs @@ -99,11 +99,59 @@ fn span_includes_args_when_no_targets() { name: X_ERROR tag: args: - 0.1 + Arg [8-11]: + value: 0.1 + targets: "#]], ); } +#[test] +fn each_arg_gets_its_own_span() { + check( + "PAULI_CHANNEL_1(0.01, 0.02, 0.03) 0", + &expect![[r#" + Circuit [0-35]: + items: + Instruction [0-35]: + name: PAULI_CHANNEL_1 + tag: + args: + Arg [16-20]: + value: 0.01 + + Arg [22-26]: + value: 0.02 + + Arg [28-32]: + value: 0.03 + + targets: + Target [34-35]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn radians_arg_span_includes_the_suffix() { + check( + "R_X(-0.5rad) 0", + &expect![[r#" + Circuit [0-14]: + items: + Instruction [0-14]: + name: R_X + tag: + args: + Arg [4-11]: + value: -0.5 rad + + targets: + Target [13-14]: + kind: Qubit(0)"#]], + ); +} + #[test] fn span_includes_tag_when_no_targets() { // A tag extends the instruction span even when there are no targets. @@ -127,16 +175,18 @@ fn span_extends_past_tag_and_args_to_target() { check( "X_ERROR[t](0.1) 5\n", &expect![[r#" - Circuit [0-18]: - items: - Instruction [0-17]: - name: X_ERROR - tag: t - args: - 0.1 - targets: - Target [16-17]: - kind: Qubit(5)"#]], + Circuit [0-18]: + items: + Instruction [0-17]: + name: X_ERROR + tag: t + args: + Arg [11-14]: + value: 0.1 + + targets: + Target [16-17]: + kind: Qubit(5)"#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/tags.rs b/source/compiler/stim_compiler/src/parser/tests/tags.rs index 013de216fac..ad90a395371 100644 --- a/source/compiler/stim_compiler/src/parser/tests/tags.rs +++ b/source/compiler/stim_compiler/src/parser/tests/tags.rs @@ -59,16 +59,18 @@ fn tag_with_args_and_targets() { check( "X_ERROR[t](0.1) 0", &expect![[r#" - Circuit [0-17]: - items: - Instruction [0-17]: - name: X_ERROR - tag: t - args: - 0.1 - targets: - Target [16-17]: - kind: Qubit(0)"#]], + Circuit [0-17]: + items: + Instruction [0-17]: + name: X_ERROR + tag: t + args: + Arg [11-14]: + value: 0.1 + + targets: + Target [16-17]: + kind: Qubit(0)"#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/targets.rs b/source/compiler/stim_compiler/src/parser/tests/targets.rs index 87a661abb69..79c744836c3 100644 --- a/source/compiler/stim_compiler/src/parser/tests/targets.rs +++ b/source/compiler/stim_compiler/src/parser/tests/targets.rs @@ -439,16 +439,18 @@ fn loss_target() { check( "E(0.01) L0", &expect![[r#" - Circuit [0-10]: - items: - Instruction [0-10]: - name: E - tag: - args: - 0.01 - targets: - Target [8-10]: - kind: Loss(0)"#]], + Circuit [0-10]: + items: + Instruction [0-10]: + name: E + tag: + args: + Arg [2-6]: + value: 0.01 + + targets: + Target [8-10]: + kind: Loss(0)"#]], ); } From 6f09106d8ea5bc85d0cf48a9ec2bb02798422d9d Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 26 Aug 2026 15:17:19 -0700 Subject: [PATCH 07/18] stim compiler recognizes angles in radians --- source/compiler/stim_compiler/src/qir.rs | 210 +++++++++++------- .../src/qir/tests/collapsing_gates.rs | 49 +++- .../tests/generalized_pauli_product_gates.rs | 44 ++++ .../src/qir/tests/noise_channels.rs | 184 +++++++++------ .../qir/tests/noise_channels_broadcasting.rs | 28 +-- .../src/qir/tests/non_clifford_gates.rs | 147 ++++++++---- .../src/qir/tests/pair_measurements.rs | 44 ++++ .../stim_compiler/src/qir/tests/peek_loss.rs | 44 ++++ 8 files changed, 537 insertions(+), 213 deletions(-) diff --git a/source/compiler/stim_compiler/src/qir.rs b/source/compiler/stim_compiler/src/qir.rs index a18fd4355b0..aa25a8ac514 100644 --- a/source/compiler/stim_compiler/src/qir.rs +++ b/source/compiler/stim_compiler/src/qir.rs @@ -22,9 +22,7 @@ type StimQubitId = u32; type QubitId = u32; type ResultId = u32; -// Angle units -type HalfTurns = f64; // used in qdk-stim -type Radians = f64; // used in QIR +type Radians = f64; struct QirWriter { output: String, @@ -386,6 +384,13 @@ pub enum Error { #[label] span: Span, }, + #[error("argument for {instruction} cannot be specified in radians")] + #[diagnostic(code("Qdk.Stim.Compiler.UnexpectedRadians"))] + UnexpectedRadians { + instruction: String, + #[label] + span: Span, + }, #[error("missing argument in instruction: {instruction}")] #[diagnostic(code("Qdk.Stim.Compiler.MissingArg"))] MissingArg { @@ -393,38 +398,34 @@ pub enum Error { #[label] span: Span, }, - #[error("instruction {instruction} requires {expected} arguments, but found {found}")] - #[diagnostic(code("Qdk.Stim.Compiler.WrongArgCount"))] - WrongArgCount { + #[error("too few arguments for instruction {instruction}; expected {expected}, found {found}")] + #[diagnostic(code("Qdk.Stim.Compiler.TooFewArgs"))] + TooFewArgs { instruction: String, expected: usize, found: usize, #[label] span: Span, }, - #[error( - "angle for {instruction} must be finite and representable in radians; found {angle} half turns" - )] - #[diagnostic(code("Qdk.Stim.Compiler.InvalidAngle"))] - InvalidAngle { + #[error("too many arguments for instruction {instruction}; expected {expected}, found {found}")] + #[diagnostic(code("Qdk.Stim.Compiler.TooManyArgs"))] + TooManyArgs { instruction: String, - angle: HalfTurns, + expected: usize, + found: usize, #[label] span: Span, }, - #[error("too many arguments for instruction {instruction}; expected at most {expected}")] - #[diagnostic(code("Qdk.Stim.Compiler.TooManyArgs"))] - TooManyArgs { + #[error("angle for {instruction} must be finite and representable in radians")] + #[diagnostic(code("Qdk.Stim.Compiler.InvalidAngle"))] + InvalidAngle { instruction: String, - expected: usize, #[label] span: Span, }, - #[error( - "readout noise probability for {instruction} must be between 0 and 1; found {probability}" - )] - #[diagnostic(code("Qdk.Stim.Compiler.InvalidReadoutNoiseProbability"))] - InvalidReadoutNoiseProbability { + #[error("probability for {instruction} must be between 0 and 1; found {probability}")] + #[diagnostic(code("Qdk.Stim.Compiler.InvalidProbability"))] + InvalidProbability { instruction: String, probability: f64, #[label] @@ -1250,7 +1251,7 @@ impl<'noise> Compiler<'noise> { } "I_ERROR" => (), "PAULI_CHANNEL_1" => { - let Some(probabilities) = self.expect_args(instruction, 3) else { + let Some(probabilities) = self.expect_probabilities(instruction, 3) else { return; }; let Some(table) = self.build_noise_table( @@ -1266,7 +1267,7 @@ impl<'noise> Compiler<'noise> { }); } "PAULI_CHANNEL_2" => { - let Some(probabilities) = self.expect_args(instruction, 15) else { + let Some(probabilities) = self.expect_probabilities(instruction, 15) else { return; }; @@ -1410,7 +1411,7 @@ impl<'noise> Compiler<'noise> { // Miscellaneous "PEEK_LOSS" => { // similar to broadcast_measure, but doesn't allow negated qubits - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; self.for_each_qubit(instruction, |s, q| { @@ -1623,7 +1624,7 @@ impl<'noise> Compiler<'noise> { instruction: &Instruction, mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; self.for_each_negatable_qubit(instruction, |s, q, negated| { @@ -1637,7 +1638,7 @@ impl<'noise> Compiler<'noise> { instruction: &Instruction, mut measure: impl FnMut(&mut Self, StimQubitId, StimQubitId, bool) -> ResultId, ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; self.for_each_negatable_pair(instruction, |s, q0, q1, negated| { @@ -1651,7 +1652,7 @@ impl<'noise> Compiler<'noise> { instruction: &Instruction, mut noise: impl FnMut(&mut Self, StimQubitId, f64), ) { - let Some(probability) = self.expect_arg(instruction) else { + let Some(probability) = self.expect_probability(instruction) else { return; }; self.for_each_qubit(instruction, |s, q| noise(s, q, probability)); @@ -1662,7 +1663,7 @@ impl<'noise> Compiler<'noise> { instruction: &Instruction, mut noise: impl FnMut(&mut Self, StimQubitId, StimQubitId, f64), ) { - let Some(probability) = self.expect_arg(instruction) else { + let Some(probability) = self.expect_probability(instruction) else { return; }; self.for_each_pair(instruction, |s, q0, q1| noise(s, q0, q1, probability)); @@ -1682,7 +1683,7 @@ impl<'noise> Compiler<'noise> { instruction: &Instruction, mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; self.for_each_pauli_product(instruction, |s, q, negated| { @@ -1804,7 +1805,7 @@ impl<'noise> Compiler<'noise> { } fn accumulate_correlated_noise(&mut self, instruction: &Instruction) { - let Some(probability) = self.expect_arg(instruction) else { + let Some(probability) = self.expect_probability(instruction) else { return; }; let mut terms = Vec::with_capacity(instruction.targets.len()); @@ -2305,6 +2306,34 @@ impl<'noise> Compiler<'noise> { } } + fn expect_target_pairs<'a>( + &mut self, + instruction: &'a Instruction, + ) -> Option> { + if !instruction.targets.len().is_multiple_of(2) { + self.push_error(Error::OddTargetCount { + instruction: instruction.name.clone(), + span: instruction.span, + }); + return None; + } + Some(instruction.targets.chunks(2)) + } + + fn expect_target_triples<'a>( + &mut self, + instruction: &'a Instruction, + ) -> Option> { + if !instruction.targets.len().is_multiple_of(3) { + self.push_error(Error::TargetCountNotMultipleOfThree { + instruction: instruction.name.clone(), + span: instruction.span, + }); + return None; + } + Some(instruction.targets.chunks(3)) + } + fn expect_angle(&mut self, instruction: &Instruction) -> Option { self.expect_angles(instruction, 1)?.pop() } @@ -2314,22 +2343,27 @@ impl<'noise> Compiler<'noise> { instruction: &Instruction, expected: usize, ) -> Option> { - let angles: Vec = self.expect_args(instruction, expected)?; - let mut radians = Vec::with_capacity(angles.len()); + let args = self.expect_args(instruction, expected)?; + let mut radians = Vec::with_capacity(args.len()); let mut has_invalid_angle = false; - for angle in angles { - let angle_in_radians = angle * PI; + + for arg in args { + let angle_in_radians = match arg.value { + ArgValue::Default(half_turns) => half_turns * PI, + ArgValue::Radians(radians) => radians, + }; + if angle_in_radians.is_finite() { radians.push(angle_in_radians); } else { self.push_error(Error::InvalidAngle { instruction: instruction.name.clone(), - angle, - span: instruction.span, + span: arg.span, }); has_invalid_angle = true; } } + if !has_invalid_angle { Some(radians) } else { @@ -2337,80 +2371,84 @@ impl<'noise> Compiler<'noise> { } } - fn expect_arg(&mut self, instruction: &Instruction) -> Option { - self.expect_args(instruction, 1).map(|args| args[0]) - } - - fn expect_args(&mut self, instruction: &Instruction, expected: usize) -> Option> { + fn expect_probability_or_zero(&mut self, instruction: &Instruction) -> Option { if instruction.args.is_empty() { - self.push_error(Error::MissingArg { - instruction: instruction.name.clone(), - span: instruction.span, - }); - return None; - } - if instruction.args.len() != expected { - self.push_error(Error::WrongArgCount { - instruction: instruction.name.clone(), - expected, - found: instruction.args.len(), - span: instruction.span, - }); - return None; + return Some(0.0); } - Some(instruction.args.clone()) + self.expect_probability(instruction) } - fn expect_target_pairs<'a>( + fn expect_probability(&mut self, instruction: &Instruction) -> Option { + self.expect_probabilities(instruction, 1)?.pop() + } + + fn expect_probabilities( &mut self, - instruction: &'a Instruction, - ) -> Option> { - if !instruction.targets.len().is_multiple_of(2) { - self.push_error(Error::OddTargetCount { - instruction: instruction.name.clone(), - span: instruction.span, - }); - return None; + instruction: &Instruction, + expected: usize, + ) -> Option> { + let args = self.expect_args(instruction, expected)?; + + let mut probabilities = Vec::with_capacity(args.len()); + let mut has_invalid_probability = false; + for arg in args { + match arg.value { + ArgValue::Default(value) => { + if (0.0..=1.0).contains(&value) { + probabilities.push(value); + } else { + self.push_error(Error::InvalidProbability { + instruction: instruction.name.clone(), + probability: value, + span: instruction.span, + }); + has_invalid_probability = true; + } + } + ArgValue::Radians(_) => { + self.push_error(Error::UnexpectedRadians { + instruction: instruction.name.clone(), + span: arg.span, + }); + has_invalid_probability = true; + } + } + } + if !has_invalid_probability { + Some(probabilities) + } else { + None } - Some(instruction.targets.chunks(2)) } - fn expect_target_triples<'a>( - &mut self, - instruction: &'a Instruction, - ) -> Option> { - if !instruction.targets.len().is_multiple_of(3) { - self.push_error(Error::TargetCountNotMultipleOfThree { + fn expect_args(&mut self, instruction: &Instruction, expected: usize) -> Option> { + let args = &instruction.args; + if args.is_empty() { + self.push_error(Error::MissingArg { instruction: instruction.name.clone(), span: instruction.span, }); return None; } - Some(instruction.targets.chunks(3)) - } - fn expect_readout_noise(&mut self, instruction: &Instruction) -> Option { - if instruction.args.len() > 1 { + if args.len() > expected { self.push_error(Error::TooManyArgs { instruction: instruction.name.clone(), - expected: 1, + expected, + found: args.len(), span: instruction.span, }); return None; - } - if instruction.args.is_empty() { - return Some(0.0); - } - let arg = instruction.args[0]; - if !(0.0..=1.0).contains(&arg) { - self.push_error(Error::InvalidReadoutNoiseProbability { + } else if args.len() < expected { + self.push_error(Error::TooFewArgs { instruction: instruction.name.clone(), - probability: arg, + expected, + found: args.len(), span: instruction.span, }); return None; } - Some(arg) + Some(args.clone()) } fn unsupported(&mut self, instruction: &Instruction) { diff --git a/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs b/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs index ce2e8a430c6..463120820f8 100644 --- a/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs +++ b/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs @@ -166,33 +166,62 @@ fn m_gate_with_invalid_readout_noise_yields_error() { check( "M(1.1) 0", &expect![[r#" - Qdk.Stim.Compiler.InvalidReadoutNoiseProbability + Qdk.Stim.Compiler.InvalidProbability + + x probability for M must be between 0 and 1; found 1.1 + ,---- + 1 | M(1.1) 0 + : ^^^^^^^^ + `---- + "#]], + ); + + check( + "M(-0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability - x readout noise probability for M must be between 0 and 1; found 1.1 + x probability for M must be between 0 and 1; found -0.1 ,---- - 1 | M(1.1) 0 - : ^^^^^^^^ + 1 | M(-0.1) 0 + : ^^^^^^^^^ `---- "#]], ); } #[test] -fn m_gate_with_two_args_yields_error() { +fn m_gate_with_readout_noise_in_radians_yields_error() { check( - "M(0.1, 0.2) 0", + "M(0.1rad) 0", &expect![[r#" - Qdk.Stim.Compiler.TooManyArgs + Qdk.Stim.Compiler.UnexpectedRadians - x too many arguments for instruction M; expected at most 1 + x argument for M cannot be specified in radians ,---- - 1 | M(0.1, 0.2) 0 - : ^^^^^^^^^^^^^ + 1 | M(0.1rad) 0 + : ^^^^^^ `---- "#]], ); } +#[test] +fn m_gate_with_two_args_yields_error() { + check( + "M(0.1, 0.2) 0", + &expect![[r#" + Qdk.Stim.Compiler.TooManyArgs + + x too many arguments for instruction M; expected 1, found 2 + ,---- + 1 | M(0.1, 0.2) 0 + : ^^^^^^^^^^^^^ + `---- + "#]], + ); +} + #[test] fn mr_gate_yields_expected_qir() { let source = "MR 0"; diff --git a/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs b/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs index 5c82fff6bf5..b511daaa35f 100644 --- a/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs +++ b/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs @@ -919,6 +919,50 @@ fn mpp_with_readout_noise_yields_expected_qir() { ); } +#[test] +fn mpp_with_invalid_readout_noise_yields_error() { + check( + "MPP(1.1) Z1*Z2", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MPP must be between 0 and 1; found 1.1 + ,---- + 1 | MPP(1.1) Z1*Z2 + : ^^^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "MPP(-0.1) Z1*Z2", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MPP must be between 0 and 1; found -0.1 + ,---- + 1 | MPP(-0.1) Z1*Z2 + : ^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn mpp_with_readout_noise_in_radians_yields_error() { + check( + "MPP(0.01rad) Z1*Z2", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for MPP cannot be specified in radians + ,---- + 1 | MPP(0.01rad) Z1*Z2 + : ^^^^^^^ + `---- + "#]], + ); +} + #[test] fn spp_single_z_yields_expected_qir() { check( diff --git a/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs b/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs index 7fdb3802c09..54f9a11b45f 100644 --- a/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs +++ b/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs @@ -115,17 +115,45 @@ fn correlated_error_without_probability_yields_error() { } #[test] -fn correlated_error_with_probability_exceeding_one_yields_error() { +fn correlated_error_with_invalid_probability_yields_error() { let source = "CORRELATED_ERROR(1.5) X0"; check( source, &expect![[r#" - Qdk.Stim.Compiler.NoiseProbabilitiesExceedOne + Qdk.Stim.Compiler.InvalidProbability - x noise probabilities must sum to at most 1.0, but they sum to 1.5 + x probability for CORRELATED_ERROR must be between 0 and 1; found 1.5 + ,---- + 1 | CORRELATED_ERROR(1.5) X0 + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "CORRELATED_ERROR(-0.1) X0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for CORRELATED_ERROR must be between 0 and 1; found -0.1 ,---- - 1 | CORRELATED_ERROR(1.5) X0 - : ^^^^^^^^^^^^^^^^^^^^^^^^ + 1 | CORRELATED_ERROR(-0.1) X0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn correlated_error_with_probability_in_radians_yields_error() { + check( + "CORRELATED_ERROR(0.1rad) X0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for CORRELATED_ERROR cannot be specified in radians + ,---- + 1 | CORRELATED_ERROR(0.1rad) X0 + : ^^^^^^ `---- "#]], ); @@ -177,32 +205,6 @@ fn correlated_error_with_probability_of_exactly_one_is_valid() { ); } -#[test] -fn correlated_error_chain_probability_error_spans_whole_group() { - let source = indoc! {" - TICK - CORRELATED_ERROR(0.5) X0 - ELSE_CORRELATED_ERROR(1.5) Z0 - ELSE_CORRELATED_ERROR(1.5) Z0 - TICK - "}; - check( - source, - &expect![[r#" - Qdk.Stim.Compiler.NegativeNoiseProbability - - x noise probabilities must be non-negative, but found -0.375 - ,-[2:1] - 1 | TICK - 2 | ,-> CORRELATED_ERROR(0.5) X0 - 3 | | ELSE_CORRELATED_ERROR(1.5) Z0 - 4 | `-> ELSE_CORRELATED_ERROR(1.5) Z0 - 5 | TICK - `---- - "#]], - ); -} - #[test] fn else_correlated_error_with_preceding_correlated_error_yields_expected_qir() { let source = indoc! {" @@ -540,17 +542,46 @@ fn depolarize1_without_probability_yields_error() { } #[test] -fn depolarize1_with_probabilities_exceeding_one_yields_error() { +fn depolarize1_with_invalid_probability_yields_error() { let source = "DEPOLARIZE1(1.5) 0"; check( source, &expect![[r#" - Qdk.Stim.Compiler.NoiseProbabilitiesExceedOne + Qdk.Stim.Compiler.InvalidProbability - x noise probabilities must sum to at most 1.0, but they sum to 1.5 + x probability for DEPOLARIZE1 must be between 0 and 1; found 1.5 + ,---- + 1 | DEPOLARIZE1(1.5) 0 + : ^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + + check( + "DEPOLARIZE1(-0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for DEPOLARIZE1 must be between 0 and 1; found -0.1 ,---- - 1 | DEPOLARIZE1(1.5) 0 - : ^^^^^^^^^^^^^^^^^^ + 1 | DEPOLARIZE1(-0.1) 0 + : ^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn depolarize1_with_probability_in_radians_yields_error() { + check( + "DEPOLARIZE1(0.1rad) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for DEPOLARIZE1 cannot be specified in radians + ,---- + 1 | DEPOLARIZE1(0.1rad) 0 + : ^^^^^^ `---- "#]], ); @@ -829,14 +860,14 @@ fn pauli_channel_1_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_1 requires 3 arguments, but found 2 - ,---- - 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_1; expected 3, found 2 + ,---- + 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } @@ -906,17 +937,46 @@ fn pauli_channel_1_with_probabilities_summing_to_exactly_one_is_valid() { } #[test] -fn pauli_channel_1_with_negative_probability_yields_error() { +fn pauli_channel_1_with_invalid_probability_yields_error() { let source = "PAULI_CHANNEL_1(-0.1, 0.2, 0.3) 0"; check( source, &expect![[r#" - Qdk.Stim.Compiler.NegativeNoiseProbability + Qdk.Stim.Compiler.InvalidProbability + + x probability for PAULI_CHANNEL_1 must be between 0 and 1; found -0.1 + ,---- + 1 | PAULI_CHANNEL_1(-0.1, 0.2, 0.3) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + + check( + "PAULI_CHANNEL_1(1.5, 0.0, 0.0) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for PAULI_CHANNEL_1 must be between 0 and 1; found 1.5 + ,---- + 1 | PAULI_CHANNEL_1(1.5, 0.0, 0.0) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn pauli_channel_1_with_probability_in_radians_yields_error() { + check( + "PAULI_CHANNEL_1(0.1rad, 0.2, 0.3) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians - x noise probabilities must be non-negative, but found -0.1 + x argument for PAULI_CHANNEL_1 cannot be specified in radians ,---- - 1 | PAULI_CHANNEL_1(-0.1, 0.2, 0.3) 0 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 1 | PAULI_CHANNEL_1(0.1rad, 0.2, 0.3) 0 + : ^^^^^^ `---- "#]], ); @@ -1005,14 +1065,14 @@ fn pauli_channel_2_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_2 requires 15 arguments, but found 1 - ,---- - 1 | PAULI_CHANNEL_2(0.1) 0 1 - : ^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_2; expected 15, found 1 + ,---- + 1 | PAULI_CHANNEL_2(0.1) 0 1 + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } @@ -1085,14 +1145,14 @@ fn x_error_with_probability_exceeding_one_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.NoiseProbabilitiesExceedOne + Qdk.Stim.Compiler.InvalidProbability - x noise probabilities must sum to at most 1.0, but they sum to 1.5 - ,---- - 1 | X_ERROR(1.5) 0 - : ^^^^^^^^^^^^^^ - `---- - "#]], + x probability for X_ERROR must be between 0 and 1; found 1.5 + ,---- + 1 | X_ERROR(1.5) 0 + : ^^^^^^^^^^^^^^ + `---- + "#]], ); } diff --git a/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs b/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs index ddc5309dce7..7496fb76f6e 100644 --- a/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs +++ b/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs @@ -346,14 +346,14 @@ fn pauli_channel_1_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_1 requires 3 arguments, but found 2 - ,---- - 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 1 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_1; expected 3, found 2 + ,---- + 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 1 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } @@ -441,14 +441,14 @@ fn pauli_channel_2_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_2 requires 15 arguments, but found 1 - ,---- - 1 | PAULI_CHANNEL_2(0.1) 0 1 2 3 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_2; expected 15, found 1 + ,---- + 1 | PAULI_CHANNEL_2(0.1) 0 1 2 3 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } diff --git a/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs b/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs index 9a974ebaca2..ac2a9e7f866 100644 --- a/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs +++ b/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs @@ -887,6 +887,42 @@ fn r_x_yields_expected_qir() { ); } +#[test] +fn r_x_with_angle_in_radians_yields_expected_qir() { + check( + "R_X(1rad) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rx__body(double 1.0, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rx__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + #[test] fn r_y_yields_expected_qir() { check( @@ -1018,9 +1054,9 @@ fn r_x_with_two_arguments_yields_error() { check( "R_X(0.25, 0.5) 0", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooManyArgs - x instruction R_X requires 1 arguments, but found 2 + x too many arguments for instruction R_X; expected 1, found 2 ,---- 1 | R_X(0.25, 0.5) 0 : ^^^^^^^^^^^^^^^^ @@ -1123,6 +1159,45 @@ fn u_alias_yields_expected_qir() { ); } +#[test] +fn u3_with_mixed_angle_units_yields_expected_qir() { + check( + "U3(0.1, -0.2rad, 3e-1rad) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.3, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double 0.3141592653589793, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double -0.2, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + #[test] fn u3_without_arguments_yields_error() { check( @@ -1144,9 +1219,9 @@ fn u3_with_one_argument_yields_error() { check( "U3(0.1) 0", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction U3 requires 3 arguments, but found 1 + x too few arguments for instruction U3; expected 3, found 1 ,---- 1 | U3(0.1) 0 : ^^^^^^^^^ @@ -1160,9 +1235,9 @@ fn u3_with_two_arguments_yields_error() { check( "U3(0.1, 0.2) 0", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction U3 requires 3 arguments, but found 2 + x too few arguments for instruction U3; expected 3, found 2 ,---- 1 | U3(0.1, 0.2) 0 : ^^^^^^^^^^^^^^ @@ -1176,9 +1251,9 @@ fn u3_with_four_arguments_yields_error() { check( "U3(0.1, 0.2, 0.3, 0.4) 0", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooManyArgs - x instruction U3 requires 3 arguments, but found 4 + x too many arguments for instruction U3; expected 3, found 4 ,---- 1 | U3(0.1, 0.2, 0.3, 0.4) 0 : ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1192,32 +1267,22 @@ fn u3_with_multiple_angles_that_overflow_radians_yields_errors() { check( "U3(1e308, 0.25, -1e308) 0", &expect![[r#" - Qdk.Stim.Compiler.InvalidAngle - - x angle for U3 must be finite and representable in radians; found - | 10000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 0000000000000 half turns - ,---- - 1 | U3(1e308, 0.25, -1e308) 0 - : ^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- + Qdk.Stim.Compiler.InvalidAngle - Qdk.Stim.Compiler.InvalidAngle + x angle for U3 must be finite and representable in radians + ,---- + 1 | U3(1e308, 0.25, -1e308) 0 + : ^^^^^ + `---- - x angle for U3 must be finite and representable in radians; found - | -1000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000 half turns - ,---- - 1 | U3(1e308, 0.25, -1e308) 0 - : ^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + Qdk.Stim.Compiler.InvalidAngle + + x angle for U3 must be finite and representable in radians + ,---- + 1 | U3(1e308, 0.25, -1e308) 0 + : ^^^^^^ + `---- + "#]], ); } @@ -1403,9 +1468,9 @@ fn r_xx_with_two_arguments_yields_error() { check( "R_XX(0.25, 0.5) 0 1", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooManyArgs - x instruction R_XX requires 1 arguments, but found 2 + x too many arguments for instruction R_XX; expected 1, found 2 ,---- 1 | R_XX(0.25, 0.5) 0 1 : ^^^^^^^^^^^^^^^^^^^ @@ -1640,13 +1705,13 @@ fn r_pauli_with_two_arguments_yields_error() { check( "R_PAULI(0.25, 0.5) X0", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooManyArgs - x instruction R_PAULI requires 1 arguments, but found 2 - ,---- - 1 | R_PAULI(0.25, 0.5) X0 - : ^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too many arguments for instruction R_PAULI; expected 1, found 2 + ,---- + 1 | R_PAULI(0.25, 0.5) X0 + : ^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } diff --git a/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs b/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs index 0faf661d0de..a4ccc5d63ce 100644 --- a/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs +++ b/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs @@ -142,6 +142,50 @@ fn mxx_with_readout_noise_yields_correct_qir() { ); } +#[test] +fn mxx_with_invalid_readout_noise_yields_error() { + check( + "MXX(1.1) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MXX must be between 0 and 1; found 1.1 + ,---- + 1 | MXX(1.1) 0 1 + : ^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "MXX(-0.1) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MXX must be between 0 and 1; found -0.1 + ,---- + 1 | MXX(-0.1) 0 1 + : ^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn mxx_with_readout_noise_in_radians_yields_error() { + check( + "MXX(0.1rad) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for MXX cannot be specified in radians + ,---- + 1 | MXX(0.1rad) 0 1 + : ^^^^^^ + `---- + "#]], + ); +} + #[test] fn myy_measurement_yields_correct_qir() { let source = "MYY 0 1"; diff --git a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs index 2c36a214d84..9576353cf60 100644 --- a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs +++ b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs @@ -124,6 +124,50 @@ fn peek_loss_with_readout_noise_yields_expected_qir() { ); } +#[test] +fn peek_loss_with_invalid_readout_noise_yields_error() { + check( + "PEEK_LOSS(1.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for PEEK_LOSS must be between 0 and 1; found 1.1 + ,---- + 1 | PEEK_LOSS(1.1) 0 + : ^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "PEEK_LOSS(-0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for PEEK_LOSS must be between 0 and 1; found -0.1 + ,---- + 1 | PEEK_LOSS(-0.1) 0 + : ^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn peek_loss_with_readout_noise_in_radians_yields_error() { + check( + "PEEK_LOSS(0.1rad) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PEEK_LOSS cannot be specified in radians + ,---- + 1 | PEEK_LOSS(0.1rad) 0 + : ^^^^^^ + `---- + "#]], + ); +} + #[test] fn peek_loss_with_negated_target_yields_error() { check( From aeaeb141588bf9db1b4b8ef109e49d43fb1391be Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 26 Aug 2026 16:08:08 -0700 Subject: [PATCH 08/18] update stim grammar to include non-clifford gates and rad --- source/vscode/syntaxes/stim.tmLanguage.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/vscode/syntaxes/stim.tmLanguage.json b/source/vscode/syntaxes/stim.tmLanguage.json index b1850a80786..3e87e0b6e06 100644 --- a/source/vscode/syntaxes/stim.tmLanguage.json +++ b/source/vscode/syntaxes/stim.tmLanguage.json @@ -68,7 +68,7 @@ "name": "keyword.other.measurement.stim" }, "gate": { - "match": "\\b(C_NXYZ|C_NZYX|C_XNYZ|C_XYNZ|C_XYZ|C_ZNYX|C_ZYNX|C_ZYX|CXSWAP|CX|CNOT|ZCX|CY|ZCY|CZSWAP|CZ|ZCZ|SWAPCZ|SWAPCX|SWAP|H_XZ|H_NXY|H_NXZ|H_NYZ|H_XY|H_YZ|H|SQRT_X_DAG|SQRT_X|SQRT_Y_DAG|SQRT_Y|SQRT_Z_DAG|SQRT_Z|SQRT_XX_DAG|SQRT_XX|SQRT_YY_DAG|SQRT_YY|SQRT_ZZ_DAG|SQRT_ZZ|S_DAG|S|ISWAP_DAG|ISWAP|II|XCX|XCY|XCZ|YCX|YCY|YCZ|MPP|SPP_DAG|SPP|I|X|Y|Z)\\b", + "match": "\\b(C_NXYZ|C_NZYX|C_XNYZ|C_XYNZ|C_XYZ|C_ZNYX|C_ZYNX|C_ZYX|CXSWAP|CX|CNOT|ZCX|CY|ZCY|CZSWAP|CZ|ZCZ|SWAPCZ|SWAPCX|SWAP|H_XZ|H_NXY|H_NXZ|H_NYZ|H_XY|H_YZ|H|SQRT_X_DAG|SQRT_X|SQRT_Y_DAG|SQRT_Y|SQRT_Z_DAG|SQRT_Z|SQRT_XX_DAG|SQRT_XX|SQRT_YY_DAG|SQRT_YY|SQRT_ZZ_DAG|SQRT_ZZ|S_DAG|S|ISWAP_DAG|ISWAP|II|XCX|XCY|XCZ|YCX|YCY|YCZ|MPP|SPP_DAG|SPP|T_DAG|T|TPP_DAG|TPP|CH|CCX|CCZ|R_X|R_Y|R_Z|U3|U|R_XX|R_YY|R_ZZ|R_PAULI|I|X|Y|Z)\\b", "name": "keyword.other.gate.stim" }, "tag": { @@ -129,7 +129,7 @@ "name": "keyword.operator.combiner.stim" }, "number": { - "match": "[+-]?\\b\\d+(\\.\\d+)?([eE][+-]?\\d+)?\\b", + "match": "[+-]?\\b\\d+(\\.\\d+)?([eE][+-]?\\d+)?(rad)?\\b", "name": "constant.numeric.stim" }, "bracket": { From 19bb65481dbd0d0925caaf9e694c89ad1d9db1eb Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 26 Aug 2026 17:05:17 -0700 Subject: [PATCH 09/18] add DecomposeCcxPass to clifford simulators --- source/qdk_package/qdk/simulation/_simulation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/qdk_package/qdk/simulation/_simulation.py b/source/qdk_package/qdk/simulation/_simulation.py index bfc92a23aec..88f1637d252 100644 --- a/source/qdk_package/qdk/simulation/_simulation.py +++ b/source/qdk_package/qdk/simulation/_simulation.py @@ -641,6 +641,7 @@ def run_qir_clifford( seed: Optional[int] = None, ) -> List: mod, shots, noise, seed = preprocess_simulation_input(input, shots, noise, seed) + DecomposeCcxPass().run(mod) if is_adaptive(mod): program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise) return run_adaptive(run_clifford_adaptive, mod, program, shots, noise, seed) From 285840f7dccd321f3e9e250529eaca9f2971c2e3 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 26 Aug 2026 17:41:25 -0700 Subject: [PATCH 10/18] add notebook for non_clifford_stim --- samples/notebooks/non_clifford_stim.ipynb | 155 ++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 samples/notebooks/non_clifford_stim.ipynb diff --git a/samples/notebooks/non_clifford_stim.ipynb b/samples/notebooks/non_clifford_stim.ipynb new file mode 100644 index 00000000000..aa59580f712 --- /dev/null +++ b/samples/notebooks/non_clifford_stim.ipynb @@ -0,0 +1,155 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2d305e92", + "metadata": {}, + "source": [ + "# Non-Clifford Stim extensions\n", + "\n", + "The gate syntax is inspired by [Clifft's](https://github.com/unitaryfoundation/clifft/blob/main/docs/reference/gates.md). `qdk.stim` supports:\n", + "\n", + "- **Phase gates:** `T`, `T_DAG`, `TPP`, `TPP_DAG`\n", + "\n", + "- **Controlled gates:** `CH`, `CCX`, `CCZ`\n", + "\n", + "- **Single-qubit rotations:** `R_X`, `R_Y`, `R_Z`, `U3`, `U`\n", + "\n", + "- **Pair rotations:** `R_XX`, `R_YY`, `R_ZZ`\n", + "\n", + "- **Pauli-product rotations:** `R_PAULI`\n", + " \n", + "\n", + "**Angles:** Unitless values are half turns: `R_X(0.5)` means $\\pi/2$ radians. Append `rad` to specify radians directly: `R_X(0.5rad)`. `U3` arguments may mix units.\n", + "\n", + "**Pauli products:** `!` negates a product, repeated same-qubit factors are folded, and anti-Hermitian products are rejected.\n", + "\n", + "**Broadcasting:** Targets are grouped as qubits, consecutive pairs or triples, or whitespace-separated Pauli products, depending on the instruction.\n", + "\n", + "**Simulation:** `type=\"clifford\"` efficiently simulates Clifford circuits with a modest amount of non-Clifford computation, such as `T` gates and Pauli rotations. To achieve that, it uses stabilizer decomposition. For more general circuits, use `type=\"cpu\"` or `type=\"gpu\"`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6952b6bc", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk import stim\n", + "from qdk.widgets import Histogram" + ] + }, + { + "cell_type": "markdown", + "id": "097cd99e", + "metadata": {}, + "source": [ + "## Phase gates\n", + "\n", + "`T_DAG` reverses `T`, so applying both leaves qubit 0 unchanged. Four `T` gates equal `Z`; on qubit 1, the surrounding Hadamard gates turn that phase flip into a bit flip. `TPP_DAG` similarly reverses `TPP` on a Pauli product." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e4bfc4b", + "metadata": {}, + "outputs": [], + "source": [ + "phase_gates = \"\"\"H 0 1\n", + "T 0\n", + "T_DAG 0\n", + "T 1\n", + "T 1\n", + "T 1\n", + "T 1\n", + "TPP X2*Z3\n", + "TPP_DAG X2*Z3\n", + "H 0 1\n", + "M 0 1 2 3\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(phase_gates, shots=2000, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "aee7f877", + "metadata": {}, + "source": [ + "## Controlled gates\n", + "\n", + "`CH` applies a Hadamard when its control is `1`. `CCX` flips its target, and `CCZ` applies a phase flip, when both controls are `1`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "352a05ab", + "metadata": {}, + "outputs": [], + "source": [ + "controlled_gates = \"\"\"X 0 1\n", + "CH 0 2\n", + "CCX 0 1 3\n", + "H 4\n", + "CCZ 0 1 4\n", + "H 4\n", + "M 0 1 2 3 4\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(controlled_gates, shots=2000, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "2affba63", + "metadata": {}, + "source": [ + "## Rotation gates\n", + "\n", + "`R_X` and `R_Y` rotate individual qubits, `R_ZZ` rotates a pair, `R_PAULI` rotates about a Pauli product, and `U` applies a general three-angle rotation,. Here, `U(1, 0, 0)` flips qubit 6. The examples use both half turns and radians." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a54885bf", + "metadata": {}, + "outputs": [], + "source": [ + "rotation_gates = \"\"\"R_X(0.5) 0\n", + "R_Y(0.5rad) 1\n", + "R_ZZ(0.25) 2 3\n", + "R_PAULI(0.25) X4*Z5\n", + "U(1, 0, 0) 6\n", + "M 0 1 2 3 4 5 6\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(rotation_gates, shots=2000, type=\"clifford\"), labels=\"kets\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.9.6)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From af5473f035fbcbc3889c7e45af104a9254006799 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 26 Aug 2026 17:45:32 -0700 Subject: [PATCH 11/18] unused display implementation --- source/compiler/stim_compiler/src/lex.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/source/compiler/stim_compiler/src/lex.rs b/source/compiler/stim_compiler/src/lex.rs index e6a43312ed4..740b9834393 100644 --- a/source/compiler/stim_compiler/src/lex.rs +++ b/source/compiler/stim_compiler/src/lex.rs @@ -104,15 +104,6 @@ pub enum DoubleUnit { Radians, } -impl Display for DoubleUnit { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - DoubleUnit::Default => f.write_str("default"), - DoubleUnit::Radians => f.write_str("radians"), - } - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum Delim { Paren, From 845b1115845e73430aa395bb608b29754f62aae2 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 27 Aug 2026 15:24:34 -0700 Subject: [PATCH 12/18] merge three separate qdk stim notebooks into a single one --- samples/notebooks/non_clifford_stim.ipynb | 155 ----- samples/notebooks/qdk_stim.ipynb | 676 ++++++++++++++++++++++ samples/notebooks/stim_select.ipynb | 261 --------- samples/notebooks/stim_to_qir.ipynb | 245 -------- 4 files changed, 676 insertions(+), 661 deletions(-) delete mode 100644 samples/notebooks/non_clifford_stim.ipynb create mode 100644 samples/notebooks/qdk_stim.ipynb delete mode 100644 samples/notebooks/stim_select.ipynb delete mode 100644 samples/notebooks/stim_to_qir.ipynb diff --git a/samples/notebooks/non_clifford_stim.ipynb b/samples/notebooks/non_clifford_stim.ipynb deleted file mode 100644 index aa59580f712..00000000000 --- a/samples/notebooks/non_clifford_stim.ipynb +++ /dev/null @@ -1,155 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2d305e92", - "metadata": {}, - "source": [ - "# Non-Clifford Stim extensions\n", - "\n", - "The gate syntax is inspired by [Clifft's](https://github.com/unitaryfoundation/clifft/blob/main/docs/reference/gates.md). `qdk.stim` supports:\n", - "\n", - "- **Phase gates:** `T`, `T_DAG`, `TPP`, `TPP_DAG`\n", - "\n", - "- **Controlled gates:** `CH`, `CCX`, `CCZ`\n", - "\n", - "- **Single-qubit rotations:** `R_X`, `R_Y`, `R_Z`, `U3`, `U`\n", - "\n", - "- **Pair rotations:** `R_XX`, `R_YY`, `R_ZZ`\n", - "\n", - "- **Pauli-product rotations:** `R_PAULI`\n", - " \n", - "\n", - "**Angles:** Unitless values are half turns: `R_X(0.5)` means $\\pi/2$ radians. Append `rad` to specify radians directly: `R_X(0.5rad)`. `U3` arguments may mix units.\n", - "\n", - "**Pauli products:** `!` negates a product, repeated same-qubit factors are folded, and anti-Hermitian products are rejected.\n", - "\n", - "**Broadcasting:** Targets are grouped as qubits, consecutive pairs or triples, or whitespace-separated Pauli products, depending on the instruction.\n", - "\n", - "**Simulation:** `type=\"clifford\"` efficiently simulates Clifford circuits with a modest amount of non-Clifford computation, such as `T` gates and Pauli rotations. To achieve that, it uses stabilizer decomposition. For more general circuits, use `type=\"cpu\"` or `type=\"gpu\"`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6952b6bc", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk import stim\n", - "from qdk.widgets import Histogram" - ] - }, - { - "cell_type": "markdown", - "id": "097cd99e", - "metadata": {}, - "source": [ - "## Phase gates\n", - "\n", - "`T_DAG` reverses `T`, so applying both leaves qubit 0 unchanged. Four `T` gates equal `Z`; on qubit 1, the surrounding Hadamard gates turn that phase flip into a bit flip. `TPP_DAG` similarly reverses `TPP` on a Pauli product." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9e4bfc4b", - "metadata": {}, - "outputs": [], - "source": [ - "phase_gates = \"\"\"H 0 1\n", - "T 0\n", - "T_DAG 0\n", - "T 1\n", - "T 1\n", - "T 1\n", - "T 1\n", - "TPP X2*Z3\n", - "TPP_DAG X2*Z3\n", - "H 0 1\n", - "M 0 1 2 3\n", - "\"\"\"\n", - "\n", - "Histogram(stim.run(phase_gates, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "aee7f877", - "metadata": {}, - "source": [ - "## Controlled gates\n", - "\n", - "`CH` applies a Hadamard when its control is `1`. `CCX` flips its target, and `CCZ` applies a phase flip, when both controls are `1`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "352a05ab", - "metadata": {}, - "outputs": [], - "source": [ - "controlled_gates = \"\"\"X 0 1\n", - "CH 0 2\n", - "CCX 0 1 3\n", - "H 4\n", - "CCZ 0 1 4\n", - "H 4\n", - "M 0 1 2 3 4\n", - "\"\"\"\n", - "\n", - "Histogram(stim.run(controlled_gates, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "2affba63", - "metadata": {}, - "source": [ - "## Rotation gates\n", - "\n", - "`R_X` and `R_Y` rotate individual qubits, `R_ZZ` rotates a pair, `R_PAULI` rotates about a Pauli product, and `U` applies a general three-angle rotation,. Here, `U(1, 0, 0)` flips qubit 6. The examples use both half turns and radians." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a54885bf", - "metadata": {}, - "outputs": [], - "source": [ - "rotation_gates = \"\"\"R_X(0.5) 0\n", - "R_Y(0.5rad) 1\n", - "R_ZZ(0.25) 2 3\n", - "R_PAULI(0.25) X4*Z5\n", - "U(1, 0, 0) 6\n", - "M 0 1 2 3 4 5 6\n", - "\"\"\"\n", - "\n", - "Histogram(stim.run(rotation_gates, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv (3.9.6)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.16" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/samples/notebooks/qdk_stim.ipynb b/samples/notebooks/qdk_stim.ipynb new file mode 100644 index 00000000000..16ed53a5fce --- /dev/null +++ b/samples/notebooks/qdk_stim.ipynb @@ -0,0 +1,676 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-00", + "metadata": {}, + "source": [ + "# Stim in the QDK\n", + "\n", + "`qdk.stim` compiles [Stim](https://github.com/quantumlib/Stim) circuits to QIR and simulates\n", + "them, with extensions for **qubit loss**, **post-selection**, and **non-Clifford gates**.\n", + "\n", + "| Function | Returns |\n", + "| --- | --- |\n", + "| `stim.compile(src, noise=None)` | `(qir, noise)` |\n", + "| `stim.run(src, shots=1, noise=None, seed=None, type=None)` | one result list per shot |\n", + "\n", + "Every measurement records `Zero`, `One`, or `Loss`, displayed below as `0`, `1`, and `L`.\n", + "\n", + "`type` picks the simulator:\n", + "\n", + "- `\"clifford\"` — stabilizer simulator. Scales to many qubits and absorbs a modest number of\n", + " non-Clifford operations by branching the stabilizer decomposition.\n", + "- `\"cpu\"` / `\"gpu\"` — full state vector, for circuits dominated by non-Clifford gates.\n", + "- `None` (default) — `\"gpu\"` when a GPU is available, otherwise `\"cpu\"`.\n", + "\n", + "> `qdk.stim` is experimental and its API may change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-01", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk import stim\n", + "from qdk.widgets import Histogram\n", + "\n", + "SHOTS = 2000" + ] + }, + { + "cell_type": "markdown", + "id": "cell-02", + "metadata": {}, + "source": [ + "## Compiling to QIR\n", + "\n", + "Everything in the [Stim gate reference](https://github.com/quantumlib/Stim/blob/main/doc/gates.md)\n", + "is supported, except for the handful of instructions listed at the end of this notebook, and\n", + "the QDK adds the extensions covered below.\n", + "\n", + "`stim.compile` lowers a circuit to QIR and returns it alongside the noise configuration that\n", + "the simulators consume." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-03", + "metadata": {}, + "outputs": [], + "source": [ + "bell = \"\"\"H 0\n", + "CX 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "qir, _ = stim.compile(bell)\n", + "print(qir)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-04", + "metadata": {}, + "source": [ + "`stim.run` compiles and simulates in one step. The Bell pair is entangled, so only `00` and\n", + "`11` occur." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-05", + "metadata": {}, + "outputs": [], + "source": [ + "Histogram(stim.run(bell, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "39fe1b22", + "metadata": {}, + "source": [ + "## Noise channels\n", + "\n", + "| Instruction | Effect |\n", + "| --- | --- |\n", + "| `X_ERROR(p)`, `Y_ERROR(p)`, `Z_ERROR(p)` | Independent Pauli error on each target |\n", + "| `DEPOLARIZE1(p)`, `DEPOLARIZE2(p)` | Uniform Pauli error per qubit or per pair |\n", + "| `PAULI_CHANNEL_1(...)`, `PAULI_CHANNEL_2(...)` | Explicit per-Pauli probabilities |\n", + "| `CORRELATED_ERROR(p)`, `E(p)` | One Pauli product applied as a single event |\n", + "| `ELSE_CORRELATED_ERROR(p)` | Another branch of the preceding correlated error |\n", + "| `LOSS_ERROR(p)` | Loses each target with probability $p$ |\n", + "\n", + "Every Pauli target on a `CORRELATED_ERROR` line belongs to one event, so `X0 X1` fires on both\n", + "qubits or on neither. `ELSE_CORRELATED_ERROR` adds a branch that is reached only when no\n", + "earlier link in the chain fired, making the branches mutually exclusive." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a51882e", + "metadata": {}, + "outputs": [], + "source": [ + "correlated = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", + "ELSE_CORRELATED_ERROR(0.2) X0\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(correlated, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "47503ec3", + "metadata": {}, + "source": [ + "### Readout noise\n", + "\n", + "Any instruction that appends to the measurement record takes an optional probability that\n", + "flips the recorded bit, leaving the qubit itself untouched: `M` / `MZ`, `MX`, `MY`, the\n", + "`MR` variants, the pair measurements `MXX` / `MYY` / `MZZ`, `MPP`, and `PEEK_LOSS`.\n", + "\n", + "Below both qubits stay in $|0\\rangle$, so every `1` in the histogram is a misread." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "756c204d", + "metadata": {}, + "outputs": [], + "source": [ + "readout_noise = \"\"\"M(0.1) 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(readout_noise, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "### Loss\n", + "\n", + "Qubit loss is a QDK extension. `LOSS_ERROR(p)` loses each target with probability $p$, and\n", + "measuring a lost qubit records `Loss` rather than a bit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-13", + "metadata": {}, + "outputs": [], + "source": [ + "loss = \"\"\"LOSS_ERROR(0.15) 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(loss, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "24ea4018", + "metadata": {}, + "source": [ + "Loss also has a target form, `L0`,\n", + "which may be combined with Pauli terms inside a correlated error to build branches that mix\n", + "the two." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-15", + "metadata": {}, + "outputs": [], + "source": [ + "mixed_loss = \"\"\"CORRELATED_ERROR(0.1) L0\n", + "ELSE_CORRELATED_ERROR(0.1) L1\n", + "ELSE_CORRELATED_ERROR(0.1) L0 X1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(mixed_loss, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-16", + "metadata": {}, + "source": [ + "### Inspecting loss with `PEEK_LOSS`\n", + "\n", + "`PEEK_LOSS` reports whether each target is currently lost, appending `1` for a lost qubit and\n", + "`0` otherwise. It neither measures the qubit nor clears the loss, so a later measurement of a\n", + "lost qubit still records `Loss`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-17", + "metadata": {}, + "outputs": [], + "source": [ + "peek = \"\"\"LOSS_ERROR(0.3) 0\n", + "PEEK_LOSS 0\n", + "M 0\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(peek, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-18", + "metadata": {}, + "source": [ + "## Post-selection with `SELECT`\n", + "\n", + "A `SELECT { ... }` block re-runs its own body until every condition inside it passes.\n", + "\n", + "- `REQUIRE rec[...]` restarts the block unless the referenced records have even parity, so\n", + " `REQUIRE rec[-1]` keeps only shots whose last measurement was `0`.\n", + "- A lost qubit has no bit to contribute to that parity, so `REQUIRE` also restarts whenever\n", + " one of its records was lost\n", + "- Prefixing a record with `!` inverts it, so `REQUIRE !rec[-1]` keeps the shots that\n", + " measured `1`.\n", + "- Conditions are checked where they appear, letting one block select several measurements in\n", + " sequence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-19", + "metadata": {}, + "outputs": [], + "source": [ + "select = \"\"\"SELECT {\n", + " H 0\n", + " M 0\n", + " REQUIRE rec[-1]\n", + " H 1\n", + " M 1\n", + " REQUIRE !rec[-1]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(select, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-20", + "metadata": {}, + "source": [ + "Listing several records in one `REQUIRE` selects on their joint parity instead of on each\n", + "record individually, which keeps the two qubits below correlated rather than fixed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-21", + "metadata": {}, + "outputs": [], + "source": [ + "parity = \"\"\"SELECT {\n", + " H 0\n", + " H 1\n", + " M 0\n", + " M 1\n", + " REQUIRE rec[-1] rec[-2]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(parity, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-22", + "metadata": {}, + "source": [ + "### Discarding lost qubits with `NOTLEAKED`\n", + "\n", + "`NOTLEAKED rec[...]` is the loss half of `REQUIRE` on its own: it restarts the block when a\n", + "referenced measurement was lost, but places no constraint on the recorded bit. Use it when a\n", + "shot should survive with either outcome, just not with `L`.\n", + "\n", + "It cannot be negated, and it cannot reference a `PEEK_LOSS` record — a peek succeeds even\n", + "when the qubit is lost, so the request would be ambiguous." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-23", + "metadata": {}, + "outputs": [], + "source": [ + "not_leaked = \"\"\"SELECT {\n", + " H 0\n", + " LOSS_ERROR(0.3) 0\n", + " MR 0\n", + " NOTLEAKED rec[-1]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(not_leaked, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-24", + "metadata": {}, + "source": [ + "### Nesting and record scope\n", + "\n", + "Blocks nest, and a restart re-runs only the body of the block that failed. A record is *in\n", + "scope* if it was produced inside the current block or a nested one; records from an enclosing\n", + "block are out of scope because a restart can no longer change them.\n", + "\n", + "Below, the inner block fixes qubit 0 and the outer block fixes qubit 1, so only `00` survives." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-25", + "metadata": {}, + "outputs": [], + "source": [ + "nested = \"\"\"SELECT {\n", + " SELECT {\n", + " H 0\n", + " M 0\n", + " REQUIRE rec[-1]\n", + " }\n", + " H 1\n", + " M 1\n", + " REQUIRE rec[-1]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(nested, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-26", + "metadata": {}, + "source": [ + "Every condition must reference at least one in-scope record, otherwise a restart could never\n", + "satisfy it and the block would loop forever; that case is rejected at compile time. Mixing an\n", + "out-of-scope record with an in-scope one is allowed, and the outer record then acts as a fixed\n", + "value to select against." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-27", + "metadata": {}, + "outputs": [], + "source": [ + "scoping = \"\"\"H 0\n", + "M 0\n", + "SELECT {\n", + " H 1\n", + " M 1\n", + " REQUIRE rec[-1] rec[-2]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(scoping, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-28", + "metadata": {}, + "source": [ + "## `REPEAT` blocks\n", + "\n", + "`REPEAT N { ... }` unrolls its body `N` times at compile time, and each iteration appends its\n", + "own measurement records. `N` must be greater than zero." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-29", + "metadata": {}, + "outputs": [], + "source": [ + "repeat = \"\"\"REPEAT 3 {\n", + " X 0\n", + " M 0\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(repeat, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-30", + "metadata": {}, + "source": [ + "## Pauli products\n", + "\n", + "`MPP` measures a Pauli product written with `*`, such as `X0*Y1*Z2`, and `SPP` / `SPP_DAG`\n", + "apply the generalized `S` gate $\\exp(\\mp i \\frac{\\pi}{4} P)$ to one. Multiple products may\n", + "share a line, separated by whitespace. The non-Clifford `TPP`, `TPP_DAG`, and `R_PAULI` take\n", + "the same targets and are covered further below.\n", + "\n", + "- A `!` on any factor negates the whole product, so `MPP !Z0*Z1` and `MPP Z0*!Z1` agree, and\n", + " `SPP !Z0` matches `SPP_DAG Z0`.\n", + "- Repeated factors on the same qubit are folded by Pauli multiplication, so `X0*Y1*Y1`\n", + " reduces to `X0`.\n", + "- Folding can leave a factor of $\\pm i$, which makes the product anti-Hermitian. Those\n", + " products, such as `X0*Z0`, are rejected.\n", + "\n", + "On a Bell pair both $Z_0Z_1$ and $X_0X_1$ measure `0` with certainty, and negating the first\n", + "product flips its outcome." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-31", + "metadata": {}, + "outputs": [], + "source": [ + "pauli_measurement = \"\"\"H 0\n", + "CX 0 1\n", + "MPP Z0*Z1 X0*X1 !Z0*Z1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(pauli_measurement, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-32", + "metadata": {}, + "source": [ + "`SPP Z` is exactly `S`. Applying it twice gives `Z`, which the surrounding Hadamards turn into\n", + "a bit flip, while `SPP Z` followed by `SPP !Z` cancels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-33", + "metadata": {}, + "outputs": [], + "source": [ + "generalized_s = \"\"\"H 0 1\n", + "SPP Z0\n", + "SPP Z0\n", + "SPP Z1\n", + "SPP !Z1\n", + "H 0 1\n", + "M 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(generalized_s, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-34", + "metadata": {}, + "source": [ + "## Non-Clifford extensions\n", + "\n", + "The QDK extends Stim with the non-Clifford gates of\n", + "[Clifft](https://github.com/unitaryfoundation/clifft/blob/main/docs/reference/gates.md).\n", + "\n", + "| Category | Instructions | Grouping |\n", + "| --- | --- | --- |\n", + "| Phase gates | `T`, `T_DAG` | one qubit each |\n", + "| | `TPP`, `TPP_DAG` | one Pauli product each |\n", + "| Controlled gates | `CH` | consecutive pairs |\n", + "| | `CCX`, `CCZ` | consecutive triples |\n", + "| Single-qubit rotations | `R_X(a)`, `R_Y(a)`, `R_Z(a)` | one qubit each |\n", + "| | `U3(t, p, l)`, `U(t, p, l)` | one qubit each |\n", + "| Pair rotations | `R_XX(a)`, `R_YY(a)`, `R_ZZ(a)` | consecutive pairs |\n", + "| Pauli rotation | `R_PAULI(a)` | one Pauli product each |\n", + "\n", + "**Angles.** A bare argument counts half turns, so `R_X(0.5)` rotates by $\\pi/2$. Append `rad`\n", + "to give radians directly, as in `R_X(0.5rad)`. `U3(theta, phi, lambda)` applies\n", + "$R_Z(\\varphi) R_Y(\\theta) R_Z(\\lambda)$ and may mix the two units. `U` is an alias for `U3`.\n", + "\n", + "**Simulation.** `type=\"clifford\"` branches the stabilizer decomposition on every non-Clifford\n", + "operation, so it stays efficient while they remain sparse. Reach for `type=\"cpu\"` or\n", + "`type=\"gpu\"` when they do not." + ] + }, + { + "cell_type": "markdown", + "id": "cell-35", + "metadata": {}, + "source": [ + "`TPP Z` is exactly `T`, so qubits 0 and 1 both accumulate $T^4 = Z$ and end up flipped.\n", + "`TPP X` instead rotates about $X$, which leaves $|+\\rangle$ alone, and the `!` on qubit 3\n", + "inverts the second gate so that the pair cancels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-36", + "metadata": {}, + "outputs": [], + "source": [ + "phase_gates = \"\"\"H 0 1 2 3\n", + "REPEAT 4 {\n", + " T 0\n", + " TPP Z1\n", + " TPP X2\n", + "}\n", + "T 3\n", + "TPP !Z3\n", + "H 0 1 2 3\n", + "M 0 1 2 3\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(phase_gates, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-37", + "metadata": {}, + "source": [ + "`CCX` flips its target and `CCZ` applies a phase flip once both controls are `1`; the\n", + "Hadamards around `CCZ` expose that phase flip in the computational basis. `CH` applies a\n", + "Hadamard when its control is `1`, leaving qubit 4 in an even superposition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-38", + "metadata": {}, + "outputs": [], + "source": [ + "controlled_gates = \"\"\"X 0 1\n", + "CCX 0 1 2\n", + "H 3\n", + "CCZ 0 1 3\n", + "H 3\n", + "CH 0 4\n", + "M 0 1 2 3 4\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(controlled_gates, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-39", + "metadata": {}, + "source": [ + "`R_X(1)` is a half turn about $X$ and flips qubit 0. `R_Y(1rad)` rotates by a single radian,\n", + "so qubit 1 measures `1` with probability $\\sin^2(1/2) \\approx 0.23$. `U(1, 0, 0)` reduces to\n", + "$R_Y(\\pi)$ and flips qubit 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-40", + "metadata": {}, + "outputs": [], + "source": [ + "rotations = \"\"\"R_X(1) 0\n", + "R_Y(1rad) 1\n", + "U(1, 0, 0) 2\n", + "M 0 1 2\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(rotations, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-41", + "metadata": {}, + "source": [ + "`R_XX(1)` and `R_PAULI(1) X*X` are the same half turn about $X \\otimes X$ and flip both of\n", + "their qubits, while `R_ZZ(0.5)` only adds a relative phase that the computational basis cannot\n", + "see." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-42", + "metadata": {}, + "outputs": [], + "source": [ + "pauli_rotations = \"\"\"R_ZZ(0.5) 0 1\n", + "R_XX(1) 2 3\n", + "R_PAULI(1) X4*X5\n", + "M 0 1 2 3 4 5\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(pauli_rotations, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-43", + "metadata": {}, + "source": [ + "## Not yet supported\n", + "\n", + "Tracked in [microsoft/qdk#3518](https://github.com/microsoft/qdk/issues/3518).\n", + "\n", + "| Feature | Current behavior |\n", + "| --- | --- |\n", + "| `HERALDED_ERASE`, `HERALDED_PAULI_CHANNEL_1` | Compile error: unsupported instruction |\n", + "| Sweep-bit targets, such as `CX sweep[5] 7` | Compile error: unsupported target |\n", + "| Pauli products that fold to the identity, such as `MPP Z0*Z0` | Compile error: unsupported target |\n", + "| `DETECTOR`, `OBSERVABLE_INCLUDE`, `QUBIT_COORDS`, `SHIFT_COORDS`, `TICK`, `MPAD` | Parsed and ignored; there is no detector or observable sampling |\n", + "| Instruction tags, such as `H[tag] 0` | Parsed and ignored |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.9.6)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/samples/notebooks/stim_select.ipynb b/samples/notebooks/stim_select.ipynb deleted file mode 100644 index 1c7cf7019a5..00000000000 --- a/samples/notebooks/stim_select.ipynb +++ /dev/null @@ -1,261 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "24104626", - "metadata": {}, - "source": [ - "# Stim `SELECT` blocks" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8148c079", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk import stim\n", - "from qdk.widgets import Histogram" - ] - }, - { - "cell_type": "markdown", - "id": "826867b4", - "metadata": {}, - "source": [ - "## Selecting a measurement outcome" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b796200d", - "metadata": {}, - "outputs": [], - "source": [ - "select_zero = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " M 0\n", - " REQUIRE rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# REQUIRE rec[-1] restarts while M 0 == 1, so every shot reports 0.\n", - "Histogram(stim.run(select_zero, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "695f6677", - "metadata": {}, - "source": [ - "## Negating a requirement with `!`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "853c5cad", - "metadata": {}, - "outputs": [], - "source": [ - "select_one = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " M 0\n", - " REQUIRE !rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Negating with `!` flips the condition: restart while M 0 == 0, so every shot reports 1.\n", - "Histogram(stim.run(select_one, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "522169df", - "metadata": {}, - "source": [ - "## Multiple `REQUIRE` statements" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9a106982", - "metadata": {}, - "outputs": [], - "source": [ - "multi_require = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " M 0\n", - " REQUIRE rec[-1]\n", - " H 1\n", - " M 1\n", - " REQUIRE rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Both qubits preselected to 0 → only 00 appears.\n", - "Histogram(stim.run(multi_require, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "0dccc4b8", - "metadata": {}, - "source": [ - "## Parity over several records" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "746161ea", - "metadata": {}, - "outputs": [], - "source": [ - "parity_check = \"\"\"\n", - "SELECT {\n", - " R 0\n", - " R 1\n", - " H 0\n", - " H 1\n", - " M 0\n", - " M 1\n", - " REQUIRE rec[-1] rec[-2]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Even parity enforced → only 00 and 11 survive.\n", - "Histogram(stim.run(parity_check, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "575121f5", - "metadata": {}, - "source": [ - "## Rejecting lost qubits with `NOTLEAKED`\n", - "\n", - "`NOTLEAKED` is the loss counterpart of `REQUIRE`: it restarts the enclosing `SELECT` block whenever a referenced measurement's qubit was *lost* instead of measured. Without it, `LOSS_ERROR` would make some shots report `L`; `NOTLEAKED` discards those so every reported shot has a genuine `0`/`1` outcome.\n", - "\n", - "Do not use `NOTLEAKED` to check a measurement record generated by a `PEEK_LOSS`. This results in an error because the request is ambiguous." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c22c1002", - "metadata": {}, - "outputs": [], - "source": [ - "survived = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " LOSS_ERROR(0.3) 0\n", - " MR 0\n", - " NOTLEAKED rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# NOTLEAKED restarts the block whenever qubit 0 is lost, so no shot reports L.\n", - "Histogram(stim.run(survived, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "13032af8", - "metadata": {}, - "source": [ - "## Nested `SELECT` blocks" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "81a0f59b", - "metadata": {}, - "outputs": [], - "source": [ - "nested = \"\"\"\n", - "SELECT {\n", - " R 1\n", - " SELECT {\n", - " R 0\n", - " H 0\n", - " M 0\n", - " REQUIRE rec[-1]\n", - " }\n", - " H 1\n", - " M 1\n", - " REQUIRE rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Inner block selects qubit 0; outer block selects qubit 1 → only 00.\n", - "Histogram(stim.run(nested, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "d502d3b4", - "metadata": {}, - "source": [ - "## Measurement record scoping\n", - "\n", - "When a `SELECT` block restarts, it re-runs **only its own body** — measurements made in an enclosing scope are not repeated. A record is *in scope* if it was produced inside the current block (or an inner one); records from an outer block are *out of scope*.\n", - "\n", - "Since a restart can only change in-scope measurements, every `REQUIRE` / `NOTLEAKED` must reference **at least one** in-scope record. Referencing only out-of-scope records could never change the outcome, so it would loop forever and is rejected at compile time.\n", - "\n", - "You *can*, however, combine an out-of-scope record with an in-scope one: the out-of-scope record acts as a fixed condition that the in-scope measurement is selected against.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "45f03f78", - "metadata": {}, - "outputs": [], - "source": [ - "scoping = \"\"\"\n", - "H 0\n", - "M 0\n", - "SELECT {\n", - " H 1\n", - " M 1\n", - " REQUIRE rec[-1] rec[-2]\n", - "}\n", - "\"\"\"\n", - "\n", - "# rec[-1] (M 1) is in scope; rec[-2] (M 0) is fixed from outside the block.\n", - "# Only M 1 is re-rolled on restart, until it matches M 0 → just 00 and 11 survive.\n", - "Histogram(stim.run(scoping, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv (3.12.12)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb deleted file mode 100644 index 96d6a701ec5..00000000000 --- a/samples/notebooks/stim_to_qir.ipynb +++ /dev/null @@ -1,245 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "772299d0", - "metadata": {}, - "source": [ - "# Stim → QIR\n", - "\n", - "Compile [Stim](https://github.com/quantumlib/Stim) circuits to QIR and simulate them with `qdk.stim`.\n", - "\n", - "- `stim.compile(src, None)` → `(qir, noise)`\n", - "- `stim.run(src, shots=..., type=\"clifford\")` → per-shot results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6c9df231", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk import stim\n", - "from qdk.widgets import Histogram" - ] - }, - { - "cell_type": "markdown", - "id": "39a2e4f8", - "metadata": {}, - "source": [ - "## Basics\n", - "\n", - "Compile a Bell pair to QIR." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a41fe64d", - "metadata": {}, - "outputs": [], - "source": [ - "bell = \"\"\"H 0\n", - "CX 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "\n", - "qir, _ = stim.compile(bell, None)\n", - "print(qir)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d859fe1a", - "metadata": {}, - "outputs": [], - "source": [ - "# Entangled: only 00 and 11 appear.\n", - "Histogram(stim.run(bell, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "ca62aa9f", - "metadata": {}, - "source": [ - "## Noise channels\n", - "The following sections detail specifying noise\n", - "\n", - "### Correlated error\n", - "\n", - "The `X0 X1` fire together → only `00` and `11`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0c2b7a13", - "metadata": {}, - "outputs": [], - "source": [ - "correlated = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(correlated, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "08be56a0", - "metadata": {}, - "source": [ - "### Pauli error\n", - "\n", - "Independent `X` on each qubit (p = 0.1)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57943533", - "metadata": {}, - "outputs": [], - "source": [ - "xerr = \"\"\"X_ERROR(0.1) 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(xerr, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "8a236142", - "metadata": {}, - "source": [ - "### Loss\n", - "\n", - "Lost qubits show up as `L` (p = 0.15)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a10524be", - "metadata": {}, - "outputs": [], - "source": [ - "loss = \"\"\"LOSS_ERROR(0.15) 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(loss, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "c53f5277", - "metadata": {}, - "source": [ - "### Inspecting loss with `PEEK_LOSS`\n", - "\n", - "`PEEK_LOSS` checks whether each target qubit is lost without measuring the qubit or changing its loss state. It appends one result per target to the measurement record: `1` if the qubit is lost and `0` otherwise. The result can be referenced with `rec[...]`.\n", - "\n", - "Like measurement instructions, `PEEK_LOSS` accepts an optional readout-noise probability argument. For example, `PEEK_LOSS(0.1) 0` flips the appended result with probability 0.1." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "acec3644", - "metadata": {}, - "outputs": [], - "source": [ - "peek_loss = \"\"\"LOSS_ERROR(1) 0\n", - "PEEK_LOSS 0\n", - "\"\"\"\n", - "Histogram(stim.run(peek_loss, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "0d45640f", - "metadata": {}, - "source": [ - "### Loss in a correlated error\n", - "\n", - "Branches mix loss (`L`) and Pauli (`X`) terms." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5aadceb8", - "metadata": {}, - "outputs": [], - "source": [ - "mixed = \"\"\"CORRELATED_ERROR(0.1) L0\n", - "ELSE_CORRELATED_ERROR(0.1) L1\n", - "ELSE_CORRELATED_ERROR(0.1) L0 X1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(mixed, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "1c40106e", - "metadata": {}, - "source": [ - "### Depolarizing\n", - "\n", - "`DEPOLARIZE1(p)`: one of 3 Paulis per qubit. `DEPOLARIZE2(p)`: one of 15 two-qubit Paulis." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10a92131", - "metadata": {}, - "outputs": [], - "source": [ - "dep1 = \"\"\"DEPOLARIZE1(0.3) 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(dep1, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c65f6416", - "metadata": {}, - "outputs": [], - "source": [ - "dep2 = \"\"\"DEPOLARIZE2(0.3) 0 1\n", - "MR 0 1\n", - "\n", - "\"\"\"\n", - "Histogram(stim.run(dep2, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv (3.14.3)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.14.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From d0583cb282fbc471ad697aa6f98b431b74da2e44 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 31 Aug 2026 11:41:21 -0700 Subject: [PATCH 13/18] remove extra line after printing arguments --- source/compiler/stim_compiler/src/parser.rs | 2 +- .../src/parser/tests/arguments.rs | 107 ++++++++---------- .../src/parser/tests/instruction_shapes.rs | 2 - .../stim_compiler/src/parser/tests/spans.rs | 58 +++++----- .../stim_compiler/src/parser/tests/tags.rs | 1 - .../stim_compiler/src/parser/tests/targets.rs | 1 - 6 files changed, 75 insertions(+), 96 deletions(-) diff --git a/source/compiler/stim_compiler/src/parser.rs b/source/compiler/stim_compiler/src/parser.rs index d6a658d9a40..8b771c61de5 100644 --- a/source/compiler/stim_compiler/src/parser.rs +++ b/source/compiler/stim_compiler/src/parser.rs @@ -103,7 +103,7 @@ pub struct Arg { impl Display for Arg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln_header_with_span(f, "Arg", self.span)?; - writeln_field(f, "value", &self.value) + write_field(f, "value", &self.value) } } diff --git a/source/compiler/stim_compiler/src/parser/tests/arguments.rs b/source/compiler/stim_compiler/src/parser/tests/arguments.rs index 54e0e6bd76c..e8d8de39599 100644 --- a/source/compiler/stim_compiler/src/parser/tests/arguments.rs +++ b/source/compiler/stim_compiler/src/parser/tests/arguments.rs @@ -17,7 +17,6 @@ fn single_arg() { args: Arg [12-17]: value: 0.001 - targets: Target [19-20]: kind: Qubit(0)"#]], @@ -37,13 +36,10 @@ fn multiple_comma_separated_args() { args: Arg [16-20]: value: 0.01 - Arg [22-26]: value: 0.02 - Arg [28-32]: value: 0.03 - targets: Target [34-35]: kind: Qubit(0)"#]], @@ -63,7 +59,6 @@ fn scientific_notation_arg() { args: Arg [8-12]: value: 0.001 - targets: Target [14-15]: kind: Qubit(0)"#]], @@ -75,50 +70,47 @@ fn radians_args() { check( "R_X(1rad) 0", &expect![[r#" - Circuit [0-11]: - items: - Instruction [0-11]: - name: R_X - tag: - args: - Arg [4-8]: - value: 1 rad - - targets: - Target [10-11]: - kind: Qubit(0)"#]], + Circuit [0-11]: + items: + Instruction [0-11]: + name: R_X + tag: + args: + Arg [4-8]: + value: 1 rad + targets: + Target [10-11]: + kind: Qubit(0)"#]], ); check( "R_Y(-0.5rad) 0", &expect![[r#" - Circuit [0-14]: - items: - Instruction [0-14]: - name: R_Y - tag: - args: - Arg [4-11]: - value: -0.5 rad - - targets: - Target [13-14]: - kind: Qubit(0)"#]], + Circuit [0-14]: + items: + Instruction [0-14]: + name: R_Y + tag: + args: + Arg [4-11]: + value: -0.5 rad + targets: + Target [13-14]: + kind: Qubit(0)"#]], ); check( "R_Z(+2.5e-3rad) 0", &expect![[r#" - Circuit [0-17]: - items: - Instruction [0-17]: - name: R_Z - tag: - args: - Arg [4-14]: - value: 0.0025 rad - - targets: - Target [16-17]: - kind: Qubit(0)"#]], + Circuit [0-17]: + items: + Instruction [0-17]: + name: R_Z + tag: + args: + Arg [4-14]: + value: 0.0025 rad + targets: + Target [16-17]: + kind: Qubit(0)"#]], ); } @@ -127,24 +119,21 @@ fn mixed_unit_args() { check( "U3(0.1, -0.2rad, 3e-1rad) 0", &expect![[r#" - Circuit [0-27]: - items: - Instruction [0-27]: - name: U3 - tag: - args: - Arg [3-6]: - value: 0.1 - - Arg [8-15]: - value: -0.2 rad - - Arg [17-24]: - value: 0.3 rad - - targets: - Target [26-27]: - kind: Qubit(0)"#]], + Circuit [0-27]: + items: + Instruction [0-27]: + name: U3 + tag: + args: + Arg [3-6]: + value: 0.1 + Arg [8-15]: + value: -0.2 rad + Arg [17-24]: + value: 0.3 rad + targets: + Target [26-27]: + kind: Qubit(0)"#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs index 3dba877ffe0..e97040d968b 100644 --- a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs +++ b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs @@ -54,7 +54,6 @@ fn args_no_targets() { args: Arg [8-11]: value: 0.1 - targets: "#]], ); } @@ -72,7 +71,6 @@ fn args_with_targets() { args: Arg [8-11]: value: 0.1 - targets: Target [13-14]: kind: Qubit(0) diff --git a/source/compiler/stim_compiler/src/parser/tests/spans.rs b/source/compiler/stim_compiler/src/parser/tests/spans.rs index 6774e61fee8..dfeb2763b88 100644 --- a/source/compiler/stim_compiler/src/parser/tests/spans.rs +++ b/source/compiler/stim_compiler/src/parser/tests/spans.rs @@ -101,7 +101,6 @@ fn span_includes_args_when_no_targets() { args: Arg [8-11]: value: 0.1 - targets: "#]], ); } @@ -111,24 +110,21 @@ fn each_arg_gets_its_own_span() { check( "PAULI_CHANNEL_1(0.01, 0.02, 0.03) 0", &expect![[r#" - Circuit [0-35]: - items: - Instruction [0-35]: - name: PAULI_CHANNEL_1 - tag: - args: - Arg [16-20]: - value: 0.01 - - Arg [22-26]: - value: 0.02 - - Arg [28-32]: - value: 0.03 - - targets: - Target [34-35]: - kind: Qubit(0)"#]], + Circuit [0-35]: + items: + Instruction [0-35]: + name: PAULI_CHANNEL_1 + tag: + args: + Arg [16-20]: + value: 0.01 + Arg [22-26]: + value: 0.02 + Arg [28-32]: + value: 0.03 + targets: + Target [34-35]: + kind: Qubit(0)"#]], ); } @@ -137,18 +133,17 @@ fn radians_arg_span_includes_the_suffix() { check( "R_X(-0.5rad) 0", &expect![[r#" - Circuit [0-14]: - items: - Instruction [0-14]: - name: R_X - tag: - args: - Arg [4-11]: - value: -0.5 rad - - targets: - Target [13-14]: - kind: Qubit(0)"#]], + Circuit [0-14]: + items: + Instruction [0-14]: + name: R_X + tag: + args: + Arg [4-11]: + value: -0.5 rad + targets: + Target [13-14]: + kind: Qubit(0)"#]], ); } @@ -183,7 +178,6 @@ fn span_extends_past_tag_and_args_to_target() { args: Arg [11-14]: value: 0.1 - targets: Target [16-17]: kind: Qubit(5)"#]], diff --git a/source/compiler/stim_compiler/src/parser/tests/tags.rs b/source/compiler/stim_compiler/src/parser/tests/tags.rs index ad90a395371..584aa04a541 100644 --- a/source/compiler/stim_compiler/src/parser/tests/tags.rs +++ b/source/compiler/stim_compiler/src/parser/tests/tags.rs @@ -67,7 +67,6 @@ fn tag_with_args_and_targets() { args: Arg [11-14]: value: 0.1 - targets: Target [16-17]: kind: Qubit(0)"#]], diff --git a/source/compiler/stim_compiler/src/parser/tests/targets.rs b/source/compiler/stim_compiler/src/parser/tests/targets.rs index 79c744836c3..5f31a7c9f64 100644 --- a/source/compiler/stim_compiler/src/parser/tests/targets.rs +++ b/source/compiler/stim_compiler/src/parser/tests/targets.rs @@ -447,7 +447,6 @@ fn loss_target() { args: Arg [2-6]: value: 0.01 - targets: Target [8-10]: kind: Loss(0)"#]], From fa37e00145eafd63bc381a4a30d1d46661c288b6 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 31 Aug 2026 11:47:17 -0700 Subject: [PATCH 14/18] make display for arg more compact --- source/compiler/stim_compiler/src/parser.rs | 3 +- .../src/parser/tests/arguments.rs | 33 +++++++------------ .../src/parser/tests/instruction_shapes.rs | 6 ++-- .../stim_compiler/src/parser/tests/spans.rs | 18 ++++------ .../stim_compiler/src/parser/tests/tags.rs | 3 +- .../stim_compiler/src/parser/tests/targets.rs | 3 +- 6 files changed, 22 insertions(+), 44 deletions(-) diff --git a/source/compiler/stim_compiler/src/parser.rs b/source/compiler/stim_compiler/src/parser.rs index 8b771c61de5..f4fbfd6d485 100644 --- a/source/compiler/stim_compiler/src/parser.rs +++ b/source/compiler/stim_compiler/src/parser.rs @@ -102,8 +102,7 @@ pub struct Arg { impl Display for Arg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln_header_with_span(f, "Arg", self.span)?; - write_field(f, "value", &self.value) + write!(f, "Arg {}: {}", self.span, self.value) } } diff --git a/source/compiler/stim_compiler/src/parser/tests/arguments.rs b/source/compiler/stim_compiler/src/parser/tests/arguments.rs index e8d8de39599..6b149c2dc8e 100644 --- a/source/compiler/stim_compiler/src/parser/tests/arguments.rs +++ b/source/compiler/stim_compiler/src/parser/tests/arguments.rs @@ -15,8 +15,7 @@ fn single_arg() { name: DEPOLARIZE1 tag: args: - Arg [12-17]: - value: 0.001 + Arg [12-17]: 0.001 targets: Target [19-20]: kind: Qubit(0)"#]], @@ -34,12 +33,9 @@ fn multiple_comma_separated_args() { name: PAULI_CHANNEL_1 tag: args: - Arg [16-20]: - value: 0.01 - Arg [22-26]: - value: 0.02 - Arg [28-32]: - value: 0.03 + Arg [16-20]: 0.01 + Arg [22-26]: 0.02 + Arg [28-32]: 0.03 targets: Target [34-35]: kind: Qubit(0)"#]], @@ -57,8 +53,7 @@ fn scientific_notation_arg() { name: X_ERROR tag: args: - Arg [8-12]: - value: 0.001 + Arg [8-12]: 0.001 targets: Target [14-15]: kind: Qubit(0)"#]], @@ -76,8 +71,7 @@ fn radians_args() { name: R_X tag: args: - Arg [4-8]: - value: 1 rad + Arg [4-8]: 1 rad targets: Target [10-11]: kind: Qubit(0)"#]], @@ -91,8 +85,7 @@ fn radians_args() { name: R_Y tag: args: - Arg [4-11]: - value: -0.5 rad + Arg [4-11]: -0.5 rad targets: Target [13-14]: kind: Qubit(0)"#]], @@ -106,8 +99,7 @@ fn radians_args() { name: R_Z tag: args: - Arg [4-14]: - value: 0.0025 rad + Arg [4-14]: 0.0025 rad targets: Target [16-17]: kind: Qubit(0)"#]], @@ -125,12 +117,9 @@ fn mixed_unit_args() { name: U3 tag: args: - Arg [3-6]: - value: 0.1 - Arg [8-15]: - value: -0.2 rad - Arg [17-24]: - value: 0.3 rad + Arg [3-6]: 0.1 + Arg [8-15]: -0.2 rad + Arg [17-24]: 0.3 rad targets: Target [26-27]: kind: Qubit(0)"#]], diff --git a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs index e97040d968b..de44d61a986 100644 --- a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs +++ b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs @@ -52,8 +52,7 @@ fn args_no_targets() { name: X_ERROR tag: args: - Arg [8-11]: - value: 0.1 + Arg [8-11]: 0.1 targets: "#]], ); } @@ -69,8 +68,7 @@ fn args_with_targets() { name: X_ERROR tag: args: - Arg [8-11]: - value: 0.1 + Arg [8-11]: 0.1 targets: Target [13-14]: kind: Qubit(0) diff --git a/source/compiler/stim_compiler/src/parser/tests/spans.rs b/source/compiler/stim_compiler/src/parser/tests/spans.rs index dfeb2763b88..253d85007e1 100644 --- a/source/compiler/stim_compiler/src/parser/tests/spans.rs +++ b/source/compiler/stim_compiler/src/parser/tests/spans.rs @@ -99,8 +99,7 @@ fn span_includes_args_when_no_targets() { name: X_ERROR tag: args: - Arg [8-11]: - value: 0.1 + Arg [8-11]: 0.1 targets: "#]], ); } @@ -116,12 +115,9 @@ fn each_arg_gets_its_own_span() { name: PAULI_CHANNEL_1 tag: args: - Arg [16-20]: - value: 0.01 - Arg [22-26]: - value: 0.02 - Arg [28-32]: - value: 0.03 + Arg [16-20]: 0.01 + Arg [22-26]: 0.02 + Arg [28-32]: 0.03 targets: Target [34-35]: kind: Qubit(0)"#]], @@ -139,8 +135,7 @@ fn radians_arg_span_includes_the_suffix() { name: R_X tag: args: - Arg [4-11]: - value: -0.5 rad + Arg [4-11]: -0.5 rad targets: Target [13-14]: kind: Qubit(0)"#]], @@ -176,8 +171,7 @@ fn span_extends_past_tag_and_args_to_target() { name: X_ERROR tag: t args: - Arg [11-14]: - value: 0.1 + Arg [11-14]: 0.1 targets: Target [16-17]: kind: Qubit(5)"#]], diff --git a/source/compiler/stim_compiler/src/parser/tests/tags.rs b/source/compiler/stim_compiler/src/parser/tests/tags.rs index 584aa04a541..0f6cc8d68da 100644 --- a/source/compiler/stim_compiler/src/parser/tests/tags.rs +++ b/source/compiler/stim_compiler/src/parser/tests/tags.rs @@ -65,8 +65,7 @@ fn tag_with_args_and_targets() { name: X_ERROR tag: t args: - Arg [11-14]: - value: 0.1 + Arg [11-14]: 0.1 targets: Target [16-17]: kind: Qubit(0)"#]], diff --git a/source/compiler/stim_compiler/src/parser/tests/targets.rs b/source/compiler/stim_compiler/src/parser/tests/targets.rs index 5f31a7c9f64..6a1a5596de5 100644 --- a/source/compiler/stim_compiler/src/parser/tests/targets.rs +++ b/source/compiler/stim_compiler/src/parser/tests/targets.rs @@ -445,8 +445,7 @@ fn loss_target() { name: E tag: args: - Arg [2-6]: - value: 0.01 + Arg [2-6]: 0.01 targets: Target [8-10]: kind: Loss(0)"#]], From a10e50fd1c90f13de83e9ba8353182c9faff466b Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 31 Aug 2026 11:55:06 -0700 Subject: [PATCH 15/18] improve test coverage for probability in radians errors --- source/compiler/stim_compiler/src/qir.rs | 29 ++++++++++--------- .../src/qir/tests/noise_channels.rs | 24 +++++++++++++++ .../stim_compiler/src/qir/tests/peek_loss.rs | 21 ++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/source/compiler/stim_compiler/src/qir.rs b/source/compiler/stim_compiler/src/qir.rs index aa25a8ac514..5cef0aab557 100644 --- a/source/compiler/stim_compiler/src/qir.rs +++ b/source/compiler/stim_compiler/src/qir.rs @@ -2392,26 +2392,27 @@ impl<'noise> Compiler<'noise> { let mut probabilities = Vec::with_capacity(args.len()); let mut has_invalid_probability = false; for arg in args { - match arg.value { - ArgValue::Default(value) => { - if (0.0..=1.0).contains(&value) { - probabilities.push(value); - } else { - self.push_error(Error::InvalidProbability { - instruction: instruction.name.clone(), - probability: value, - span: instruction.span, - }); - has_invalid_probability = true; - } - } - ArgValue::Radians(_) => { + let value = match arg.value { + ArgValue::Default(value) => value, + ArgValue::Radians(value) => { self.push_error(Error::UnexpectedRadians { instruction: instruction.name.clone(), span: arg.span, }); has_invalid_probability = true; + value } + }; + + if (0.0..=1.0).contains(&value) { + probabilities.push(value); + } else { + self.push_error(Error::InvalidProbability { + instruction: instruction.name.clone(), + probability: value, + span: instruction.span, + }); + has_invalid_probability = true; } } if !has_invalid_probability { diff --git a/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs b/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs index 54f9a11b45f..7609bdd7321 100644 --- a/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs +++ b/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs @@ -982,6 +982,30 @@ fn pauli_channel_1_with_probability_in_radians_yields_error() { ); } +#[test] +fn pauli_channel_1_with_multiple_probabilities_in_radians_yields_errors() { + check( + "PAULI_CHANNEL_1(0.1rad, 0.2rad, 0.3) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PAULI_CHANNEL_1 cannot be specified in radians + ,---- + 1 | PAULI_CHANNEL_1(0.1rad, 0.2rad, 0.3) 0 + : ^^^^^^ + `---- + + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PAULI_CHANNEL_1 cannot be specified in radians + ,---- + 1 | PAULI_CHANNEL_1(0.1rad, 0.2rad, 0.3) 0 + : ^^^^^^ + `---- + "#]], + ); +} + #[test] fn pauli_channel_2_yields_expected_qir() { let source = "PAULI_CHANNEL_2(0,0,0, 0,0.1,0,0, 0,0,0,0.2, 0,0,0,0) 0 1"; diff --git a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs index 9576353cf60..53beb35142c 100644 --- a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs +++ b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs @@ -168,6 +168,27 @@ fn peek_loss_with_readout_noise_in_radians_yields_error() { ); } + #[test] + fn peek_loss_with_negative_readout_noise_in_radians_yields_errors() { + check("PEEK_LOSS(-0.1rad) 0", &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PEEK_LOSS cannot be specified in radians + ,---- + 1 | PEEK_LOSS(-0.1rad) 0 + : ^^^^^^^ + `---- + + Qdk.Stim.Compiler.InvalidProbability + + x probability for PEEK_LOSS must be between 0 and 1; found -0.1 + ,---- + 1 | PEEK_LOSS(-0.1rad) 0 + : ^^^^^^^^^^^^^^^^^^^^ + `---- + "#]]); + } + #[test] fn peek_loss_with_negated_target_yields_error() { check( From deefcf274999f34e385b96f03a16142e45deff83 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 31 Aug 2026 11:55:32 -0700 Subject: [PATCH 16/18] cargo fmt --- .../stim_compiler/src/qir/tests/peek_loss.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs index 53beb35142c..bbc1b6f598c 100644 --- a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs +++ b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs @@ -168,9 +168,11 @@ fn peek_loss_with_readout_noise_in_radians_yields_error() { ); } - #[test] - fn peek_loss_with_negative_readout_noise_in_radians_yields_errors() { - check("PEEK_LOSS(-0.1rad) 0", &expect![[r#" +#[test] +fn peek_loss_with_negative_readout_noise_in_radians_yields_errors() { + check( + "PEEK_LOSS(-0.1rad) 0", + &expect![[r#" Qdk.Stim.Compiler.UnexpectedRadians x argument for PEEK_LOSS cannot be specified in radians @@ -186,8 +188,9 @@ fn peek_loss_with_readout_noise_in_radians_yields_error() { 1 | PEEK_LOSS(-0.1rad) 0 : ^^^^^^^^^^^^^^^^^^^^ `---- - "#]]); - } + "#]], + ); +} #[test] fn peek_loss_with_negated_target_yields_error() { From 903c4ae4fcf5347faee0695ee3484cb95be5e0af Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 31 Aug 2026 12:03:07 -0700 Subject: [PATCH 17/18] standardize source string indenting in qdk_stim notebook --- samples/notebooks/qdk_stim.ipynb | 54 +++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/samples/notebooks/qdk_stim.ipynb b/samples/notebooks/qdk_stim.ipynb index 16ed53a5fce..7b7d9f21cb6 100644 --- a/samples/notebooks/qdk_stim.ipynb +++ b/samples/notebooks/qdk_stim.ipynb @@ -62,7 +62,8 @@ "metadata": {}, "outputs": [], "source": [ - "bell = \"\"\"H 0\n", + "bell = \"\"\"\n", + "H 0\n", "CX 0 1\n", "MR 0 1\n", "\"\"\"\n", @@ -118,7 +119,8 @@ "metadata": {}, "outputs": [], "source": [ - "correlated = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", + "correlated = \"\"\"\n", + "CORRELATED_ERROR(0.2) X0 X1\n", "ELSE_CORRELATED_ERROR(0.2) X0\n", "MR 0 1\n", "\"\"\"\n", @@ -147,7 +149,8 @@ "metadata": {}, "outputs": [], "source": [ - "readout_noise = \"\"\"M(0.1) 0 1\n", + "readout_noise = \"\"\"\n", + "M(0.1) 0 1\n", "\"\"\"\n", "\n", "Histogram(stim.run(readout_noise, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" @@ -171,7 +174,8 @@ "metadata": {}, "outputs": [], "source": [ - "loss = \"\"\"LOSS_ERROR(0.15) 0 1\n", + "loss = \"\"\"\n", + "LOSS_ERROR(0.15) 0 1\n", "MR 0 1\n", "\"\"\"\n", "\n", @@ -195,7 +199,8 @@ "metadata": {}, "outputs": [], "source": [ - "mixed_loss = \"\"\"CORRELATED_ERROR(0.1) L0\n", + "mixed_loss = \"\"\"\n", + "CORRELATED_ERROR(0.1) L0\n", "ELSE_CORRELATED_ERROR(0.1) L1\n", "ELSE_CORRELATED_ERROR(0.1) L0 X1\n", "MR 0 1\n", @@ -223,7 +228,8 @@ "metadata": {}, "outputs": [], "source": [ - "peek = \"\"\"LOSS_ERROR(0.3) 0\n", + "peek = \"\"\"\n", + "LOSS_ERROR(0.3) 0\n", "PEEK_LOSS 0\n", "M 0\n", "\"\"\"\n", @@ -257,7 +263,8 @@ "metadata": {}, "outputs": [], "source": [ - "select = \"\"\"SELECT {\n", + "select = \"\"\"\n", + "SELECT {\n", " H 0\n", " M 0\n", " REQUIRE rec[-1]\n", @@ -286,7 +293,8 @@ "metadata": {}, "outputs": [], "source": [ - "parity = \"\"\"SELECT {\n", + "parity = \"\"\"\n", + "SELECT {\n", " H 0\n", " H 1\n", " M 0\n", @@ -320,7 +328,8 @@ "metadata": {}, "outputs": [], "source": [ - "not_leaked = \"\"\"SELECT {\n", + "not_leaked = \"\"\"\n", + "SELECT {\n", " H 0\n", " LOSS_ERROR(0.3) 0\n", " MR 0\n", @@ -352,7 +361,8 @@ "metadata": {}, "outputs": [], "source": [ - "nested = \"\"\"SELECT {\n", + "nested = \"\"\"\n", + "SELECT {\n", " SELECT {\n", " H 0\n", " M 0\n", @@ -385,7 +395,8 @@ "metadata": {}, "outputs": [], "source": [ - "scoping = \"\"\"H 0\n", + "scoping = \"\"\"\n", + "H 0\n", "M 0\n", "SELECT {\n", " H 1\n", @@ -415,7 +426,8 @@ "metadata": {}, "outputs": [], "source": [ - "repeat = \"\"\"REPEAT 3 {\n", + "repeat = \"\"\"\n", + "REPEAT 3 {\n", " X 0\n", " M 0\n", "}\n", @@ -454,7 +466,8 @@ "metadata": {}, "outputs": [], "source": [ - "pauli_measurement = \"\"\"H 0\n", + "pauli_measurement = \"\"\"\n", + "H 0\n", "CX 0 1\n", "MPP Z0*Z1 X0*X1 !Z0*Z1\n", "\"\"\"\n", @@ -478,7 +491,8 @@ "metadata": {}, "outputs": [], "source": [ - "generalized_s = \"\"\"H 0 1\n", + "generalized_s = \"\"\"\n", + "H 0 1\n", "SPP Z0\n", "SPP Z0\n", "SPP Z1\n", @@ -537,7 +551,8 @@ "metadata": {}, "outputs": [], "source": [ - "phase_gates = \"\"\"H 0 1 2 3\n", + "phase_gates = \"\"\"\n", + "H 0 1 2 3\n", "REPEAT 4 {\n", " T 0\n", " TPP Z1\n", @@ -569,7 +584,8 @@ "metadata": {}, "outputs": [], "source": [ - "controlled_gates = \"\"\"X 0 1\n", + "controlled_gates = \"\"\"\n", + "X 0 1\n", "CCX 0 1 2\n", "H 3\n", "CCZ 0 1 3\n", @@ -598,7 +614,8 @@ "metadata": {}, "outputs": [], "source": [ - "rotations = \"\"\"R_X(1) 0\n", + "rotations = \"\"\"\n", + "R_X(1) 0\n", "R_Y(1rad) 1\n", "U(1, 0, 0) 2\n", "M 0 1 2\n", @@ -624,7 +641,8 @@ "metadata": {}, "outputs": [], "source": [ - "pauli_rotations = \"\"\"R_ZZ(0.5) 0 1\n", + "pauli_rotations = \"\"\"\n", + "R_ZZ(0.5) 0 1\n", "R_XX(1) 2 3\n", "R_PAULI(1) X4*X5\n", "M 0 1 2 3 4 5\n", From 8c09b01074bfa074f7fe75efc6d0a08e52bb906e Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 31 Aug 2026 12:10:14 -0700 Subject: [PATCH 18/18] update qdk stim unsupported section --- samples/notebooks/qdk_stim.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/notebooks/qdk_stim.ipynb b/samples/notebooks/qdk_stim.ipynb index 7b7d9f21cb6..5a69f73777b 100644 --- a/samples/notebooks/qdk_stim.ipynb +++ b/samples/notebooks/qdk_stim.ipynb @@ -662,11 +662,11 @@ "\n", "| Feature | Current behavior |\n", "| --- | --- |\n", + "| `MPAD` | Parsed and ignored instead of appending bits to the measurement record |\n", "| `HERALDED_ERASE`, `HERALDED_PAULI_CHANNEL_1` | Compile error: unsupported instruction |\n", - "| Sweep-bit targets, such as `CX sweep[5] 7` | Compile error: unsupported target |\n", "| Pauli products that fold to the identity, such as `MPP Z0*Z0` | Compile error: unsupported target |\n", - "| `DETECTOR`, `OBSERVABLE_INCLUDE`, `QUBIT_COORDS`, `SHIFT_COORDS`, `TICK`, `MPAD` | Parsed and ignored; there is no detector or observable sampling |\n", - "| Instruction tags, such as `H[tag] 0` | Parsed and ignored |" + "| Sweep-bit targets, such as `CX sweep[5] 7` | Compile error: unsupported target |\n", + "| `DETECTOR`, `OBSERVABLE_INCLUDE` | Parsed and ignored; there is no detector or observable sampling |" ] } ],