From a85de67f4b04246cc35353f2482c5230c9f91bbe Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 3 Aug 2026 15:07:59 -0700 Subject: [PATCH 1/6] support __quantum__qis__peek_loss__body intrinsic in adaptive cpu simulators --- source/qdk_package/qdk/_adaptive_bytecode.py | 1 + source/qdk_package/qdk/_adaptive_pass.py | 4 ++++ source/simulators/src/bytecode/runtime.rs | 10 +++++++++- source/simulators/src/cpu_full_state_simulator.rs | 8 ++++++++ source/simulators/src/lib.rs | 3 +++ source/simulators/src/stabilizer_simulator.rs | 4 ++++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/source/qdk_package/qdk/_adaptive_bytecode.py b/source/qdk_package/qdk/_adaptive_bytecode.py index a634cfde1c7..79dd17e9b97 100644 --- a/source/qdk_package/qdk/_adaptive_bytecode.py +++ b/source/qdk_package/qdk/_adaptive_bytecode.py @@ -45,6 +45,7 @@ OP_READ_RESULT = 0x13 OP_RECORD_OUTPUT = 0x14 OP_READ_LOSS = 0x15 +OP_PEEK_LOSS = 0x16 # ── Integer Arithmetic ─────────────────────────────────────────────────────── OP_ADD = 0x20 diff --git a/source/qdk_package/qdk/_adaptive_pass.py b/source/qdk_package/qdk/_adaptive_pass.py index c023f3da8b4..c6ac3931987 100644 --- a/source/qdk_package/qdk/_adaptive_pass.py +++ b/source/qdk_package/qdk/_adaptive_pass.py @@ -944,6 +944,10 @@ def _emit_call(self, call: pyqir.Call) -> None: dst = self._alloc_reg(call, REG_TYPE_BOOL) result_reg = self._resolve_result_operand(call.args[0]) self._emit(OP_READ_LOSS, dst=dst, src0=result_reg) + case "__quantum__qis__peek_loss__body": + dst = self._alloc_reg(call, REG_TYPE_BOOL) + q = self._resolve_qubit_operand(call.args[0]) + self._emit(OP_PEEK_LOSS, dst=dst, src0=q) case _ if callee.startswith("__quantum__qis__"): self._emit_quantum_call(call) case _ if callee in self._func_to_id: diff --git a/source/simulators/src/bytecode/runtime.rs b/source/simulators/src/bytecode/runtime.rs index c177c926b0d..3ac0ca25aa3 100644 --- a/source/simulators/src/bytecode/runtime.rs +++ b/source/simulators/src/bytecode/runtime.rs @@ -15,7 +15,7 @@ )] use crate::{ - MeasurementResult, OutputRecord, Simulator, + MeasurementResult, OutputRecord, QubitID, Simulator, bytecode::{AdaptiveProgram, Instruction}, }; @@ -49,6 +49,7 @@ const OP_RESET: u8 = 0x12; const OP_READ_RESULT: u8 = 0x13; const OP_RECORD_OUTPUT: u8 = 0x14; const OP_READ_LOSS: u8 = 0x15; +const OP_PEEK_LOSS: u8 = 0x16; // Integer arithmetic const OP_ADD: u8 = 0x20; @@ -474,6 +475,13 @@ pub fn run_shot(program: &AdaptiveProgram, sim: &mut S) -> Ve rt.pc += 1; } + OP_PEEK_LOSS => { + let qubit = rt.resolve_u64(instr.src0, flags, 0) as QubitID; + let val = u64::from(sim.is_qubit_lost(qubit)); + rt.write_reg(instr.dst, val); + rt.pc += 1; + } + OP_RECORD_OUTPUT => { // The record kind is encoded as an immediate in `aux1`. Every // leaf record is captured into the unified `records` stream in diff --git a/source/simulators/src/cpu_full_state_simulator.rs b/source/simulators/src/cpu_full_state_simulator.rs index 10a69cfff3b..6abff311c5f 100644 --- a/source/simulators/src/cpu_full_state_simulator.rs +++ b/source/simulators/src/cpu_full_state_simulator.rs @@ -411,6 +411,10 @@ impl Simulator for NoiselessSimulator { fn state_dump(&self) -> &Self::StateDumpData { self.state.state().expect("state should be valid") } + + fn is_qubit_lost(&self, _qubit: QubitID) -> bool { + false + } } /// A noisy state-vector simulator. @@ -1035,4 +1039,8 @@ impl Simulator for NoisySimulator { fn state_dump(&self) -> &Self::StateDumpData { self.state.state().expect("state should be valid") } + + fn is_qubit_lost(&self, qubit: QubitID) -> bool { + self.loss[qubit] + } } diff --git a/source/simulators/src/lib.rs b/source/simulators/src/lib.rs index 812cf3b46bf..acbb31a67be 100644 --- a/source/simulators/src/lib.rs +++ b/source/simulators/src/lib.rs @@ -125,4 +125,7 @@ pub trait Simulator { /// Dumps the current state of the simulator in some representation that can be compared /// for `PartialEq` up to a global phase. This is meant to be used for testing. fn state_dump(&self) -> &Self::StateDumpData; + + /// Same as `read_loss`, but doesn't collapse state. + fn is_qubit_lost(&self, qubit: QubitID) -> bool; } diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 44bf336bc30..36b006b1127 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -709,6 +709,10 @@ impl Simulator for StabilizerSimulator { fn state_dump(&self) -> &Self::StateDumpData { self.state.clifford() } + + fn is_qubit_lost(&self, qubit: QubitID) -> bool { + self.loss[qubit] + } } fn unitary_from_normalized_angle( From 37c9a5f4bca16593b6246da4665d11a51ac4f6e6 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 3 Aug 2026 15:57:34 -0700 Subject: [PATCH 2/6] add test for peek_loss --- .../tests/test_adaptive_cpu_bytecode.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/source/qdk_package/tests/test_adaptive_cpu_bytecode.py b/source/qdk_package/tests/test_adaptive_cpu_bytecode.py index 1cd016001b8..c054f4b4df1 100644 --- a/source/qdk_package/tests/test_adaptive_cpu_bytecode.py +++ b/source/qdk_package/tests/test_adaptive_cpu_bytecode.py @@ -618,6 +618,57 @@ def test_read_loss(sim_type): }, f"Expected all {SHOTS} shots to be 'L1', got {counts}" +# ========================================================================= +# OP_PEEK_LOSS — read whether a qubit is lost without collapsing +# ========================================================================= + +PEEK_LOSS_QIR = """ +entry: + ; Apply s to qubit 0 purely for its noise side effect. With + ; ``noise.s.loss = 1.0`` the simulator faults qubit 0 as lost on every shot. + call void @__quantum__qis__s__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + ; Peek the loss state of qubit 0. Unlike read_loss, this reads the qubit + ; directly and doesn't collapse the qubit. + %lost = call i1 @__quantum__qis__peek_loss__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + br i1 %lost, label %then, label %end + +then: + ; Witness: if peek_loss reported true, flip qubit 1 to |1⟩. + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + br label %end + +end: + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) +""" + +PEEK_LOSS_DECLS = """ +declare i1 @__quantum__qis__peek_loss__body(%Qubit*) +""" + + +@pytest.mark.parametrize("sim_type", SIM_TYPES) +def test_peek_loss(sim_type): + """s (with 100% loss) → peek_loss → branch on loss → mz witness. + + Result 0 should always be ``Loss`` ('L'), and result 1 should always be ``One`` ('1') because + peek_loss observed the loss and the conditional X was applied to qubit 1. + """ + qir = format_qir( + PEEK_LOSS_QIR, + extra_decls=PEEK_LOSS_DECLS, + num_qubits=2, + num_results=2, + ) + noise = NoiseConfig() + noise.s.loss = 1.0 + results = run_qir(qir, SHOTS, noise, seed=42, type=sim_type) + counts = Counter(map_result_list_to_str(r) for r in results) + assert counts == { + "L1": SHOTS + }, f"Expected all {SHOTS} shots to be 'L1', got {counts}" + + # ========================================================================= # move (OpID 28) — qubit move with associated noise # ========================================================================= From db1c4cb6b3730b808f4d3abecc2ac5ba5193d6c0 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 4 Aug 2026 13:27:01 -0700 Subject: [PATCH 3/6] change peek_loss shape to that of a measurement --- source/qdk_package/qdk/_adaptive_pass.py | 4 ++-- source/simulators/src/bytecode/runtime.rs | 6 +++--- source/simulators/src/cpu_full_state_simulator.rs | 13 +++++++++---- source/simulators/src/lib.rs | 4 ++-- source/simulators/src/stabilizer_simulator.rs | 9 +++++++-- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/source/qdk_package/qdk/_adaptive_pass.py b/source/qdk_package/qdk/_adaptive_pass.py index c6ac3931987..5ec3088a03c 100644 --- a/source/qdk_package/qdk/_adaptive_pass.py +++ b/source/qdk_package/qdk/_adaptive_pass.py @@ -945,9 +945,9 @@ def _emit_call(self, call: pyqir.Call) -> None: result_reg = self._resolve_result_operand(call.args[0]) self._emit(OP_READ_LOSS, dst=dst, src0=result_reg) case "__quantum__qis__peek_loss__body": - dst = self._alloc_reg(call, REG_TYPE_BOOL) q = self._resolve_qubit_operand(call.args[0]) - self._emit(OP_PEEK_LOSS, dst=dst, src0=q) + r = self._resolve_result_operand(call.args[1]) + self._emit(OP_PEEK_LOSS, aux0=q, aux1=r) case _ if callee.startswith("__quantum__qis__"): self._emit_quantum_call(call) case _ if callee in self._func_to_id: diff --git a/source/simulators/src/bytecode/runtime.rs b/source/simulators/src/bytecode/runtime.rs index 3ac0ca25aa3..d45c837efac 100644 --- a/source/simulators/src/bytecode/runtime.rs +++ b/source/simulators/src/bytecode/runtime.rs @@ -476,9 +476,9 @@ pub fn run_shot(program: &AdaptiveProgram, sim: &mut S) -> Ve } OP_PEEK_LOSS => { - let qubit = rt.resolve_u64(instr.src0, flags, 0) as QubitID; - let val = u64::from(sim.is_qubit_lost(qubit)); - rt.write_reg(instr.dst, val); + let target = rt.resolve_u64(instr.aux0, flags, 3) as QubitID; + let result_id = rt.resolve_u64(instr.aux1, flags, 4) as QubitID; + sim.peek_loss(target, result_id); rt.pc += 1; } diff --git a/source/simulators/src/cpu_full_state_simulator.rs b/source/simulators/src/cpu_full_state_simulator.rs index 6abff311c5f..98bd5aa21de 100644 --- a/source/simulators/src/cpu_full_state_simulator.rs +++ b/source/simulators/src/cpu_full_state_simulator.rs @@ -412,8 +412,8 @@ impl Simulator for NoiselessSimulator { self.state.state().expect("state should be valid") } - fn is_qubit_lost(&self, _qubit: QubitID) -> bool { - false + fn peek_loss(&mut self, _target: QubitID, result_id: QubitID) { + self.measurements[result_id] = MeasurementResult::Zero; // Peek loss always returns zero for the noiseless simulator. } } @@ -1040,7 +1040,12 @@ impl Simulator for NoisySimulator { self.state.state().expect("state should be valid") } - fn is_qubit_lost(&self, qubit: QubitID) -> bool { - self.loss[qubit] + fn peek_loss(&mut self, target: QubitID, result_id: QubitID) { + let is_lost = self.loss[target]; + self.measurements[result_id] = if is_lost { + MeasurementResult::One + } else { + MeasurementResult::Zero + }; } } diff --git a/source/simulators/src/lib.rs b/source/simulators/src/lib.rs index acbb31a67be..92f2c0cf93c 100644 --- a/source/simulators/src/lib.rs +++ b/source/simulators/src/lib.rs @@ -126,6 +126,6 @@ pub trait Simulator { /// for `PartialEq` up to a global phase. This is meant to be used for testing. fn state_dump(&self) -> &Self::StateDumpData; - /// Same as `read_loss`, but doesn't collapse state. - fn is_qubit_lost(&self, qubit: QubitID) -> bool; + /// Measures loss but does not collapse the state. The result is stored in `result_id`. + fn peek_loss(&mut self, target: QubitID, result_id: QubitID); } diff --git a/source/simulators/src/stabilizer_simulator.rs b/source/simulators/src/stabilizer_simulator.rs index 36b006b1127..a95e0198ea7 100644 --- a/source/simulators/src/stabilizer_simulator.rs +++ b/source/simulators/src/stabilizer_simulator.rs @@ -710,8 +710,13 @@ impl Simulator for StabilizerSimulator { self.state.clifford() } - fn is_qubit_lost(&self, qubit: QubitID) -> bool { - self.loss[qubit] + fn peek_loss(&mut self, qubit: QubitID, result_id: QubitID) { + let is_lost = self.loss[qubit]; + self.measurements[result_id] = if is_lost { + MeasurementResult::One + } else { + MeasurementResult::Zero + }; } } From 8b04653ae4574e03bd46f310be0ceb14feb79c47 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 4 Aug 2026 13:29:28 -0700 Subject: [PATCH 4/6] update adaptive_cpu_bytecode test for new shape --- .../tests/test_adaptive_cpu_bytecode.py | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/source/qdk_package/tests/test_adaptive_cpu_bytecode.py b/source/qdk_package/tests/test_adaptive_cpu_bytecode.py index c054f4b4df1..15f390a7b5d 100644 --- a/source/qdk_package/tests/test_adaptive_cpu_bytecode.py +++ b/source/qdk_package/tests/test_adaptive_cpu_bytecode.py @@ -624,40 +624,33 @@ def test_read_loss(sim_type): PEEK_LOSS_QIR = """ entry: - ; Apply s to qubit 0 purely for its noise side effect. With - ; ``noise.s.loss = 1.0`` the simulator faults qubit 0 as lost on every shot. + ; Apply s to qubit 0 for its noise side effect. With ``noise.s.loss = 1.0`` + ; the simulator faults qubit 0 as lost on every shot. call void @__quantum__qis__s__body(%Qubit* inttoptr (i64 0 to %Qubit*)) - ; Peek the loss state of qubit 0. Unlike read_loss, this reads the qubit - ; directly and doesn't collapse the qubit. - %lost = call i1 @__quantum__qis__peek_loss__body(%Qubit* inttoptr (i64 0 to %Qubit*)) - br i1 %lost, label %then, label %end - -then: - ; Witness: if peek_loss reported true, flip qubit 1 to |1⟩. - call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 1 to %Qubit*)) - br label %end - -end: - call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) - call void @__quantum__qis__mresetz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + ; peek_loss behaves like a measurement: it records the loss status of qubit 0 + ; into result 0 (One when lost) but does not collapse the qubit. + call void @__quantum__qis__peek_loss__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + ; Because peek_loss did not clear the loss, a later mz on qubit 0 still records Loss. + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) """ PEEK_LOSS_DECLS = """ -declare i1 @__quantum__qis__peek_loss__body(%Qubit*) +declare void @__quantum__qis__peek_loss__body(%Qubit*, %Result*) """ @pytest.mark.parametrize("sim_type", SIM_TYPES) def test_peek_loss(sim_type): - """s (with 100% loss) → peek_loss → branch on loss → mz witness. + """s (with 100% loss) → peek_loss records loss into a result without collapsing. - Result 0 should always be ``Loss`` ('L'), and result 1 should always be ``One`` ('1') because - peek_loss observed the loss and the conditional X was applied to qubit 1. + Result 0 is the peek: ``One`` ('1') because qubit 0 is lost. Result 1 is a + later mz on the same qubit: ``Loss`` ('L'), showing peek_loss did not clear + the loss — it behaves like a measurement but does not collapse state. """ qir = format_qir( PEEK_LOSS_QIR, extra_decls=PEEK_LOSS_DECLS, - num_qubits=2, + num_qubits=1, num_results=2, ) noise = NoiseConfig() @@ -665,8 +658,8 @@ def test_peek_loss(sim_type): results = run_qir(qir, SHOTS, noise, seed=42, type=sim_type) counts = Counter(map_result_list_to_str(r) for r in results) assert counts == { - "L1": SHOTS - }, f"Expected all {SHOTS} shots to be 'L1', got {counts}" + "1L": SHOTS + }, f"Expected all {SHOTS} shots to be '1L', got {counts}" # ========================================================================= From 79a80a56b618750b655c8ad2a5e8c92b50c86ecc Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 4 Aug 2026 16:52:57 -0700 Subject: [PATCH 5/6] support peek_loss in stim --- source/compiler/stim_compiler/src/qir.rs | 34 ++++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/source/compiler/stim_compiler/src/qir.rs b/source/compiler/stim_compiler/src/qir.rs index 545e4e16ae4..3c9d7c76907 100644 --- a/source/compiler/stim_compiler/src/qir.rs +++ b/source/compiler/stim_compiler/src/qir.rs @@ -11,7 +11,7 @@ use qdk_simulators::noise_config::{ use crate::parser::*; use miette::Diagnostic; use qsc_data_structures::span::Span; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use std::fmt::Write; use std::slice::Chunks; use thiserror::Error; @@ -356,6 +356,12 @@ pub enum Error { #[label] span: Span, }, + #[error("NOTLEAKED cannot reference a record produced by PEEK_LOSS")] + #[diagnostic(code("Qdk.Stim.Compiler.NotLeakedOnPeekLoss"))] + NotLeakedOnPeekLoss { + #[label] + span: Span, + }, #[error( "controlled instruction {instruction} requires a qubit target, but both targets are measurement records" )] @@ -476,6 +482,7 @@ struct IdMap { qubit_map: FxHashMap, name_counters: FxHashMap<&'static str, u32>, // prefix -> next index record_count: u32, // number of allocated measurement records + peek_loss_record_ids: FxHashSet, // record ids produced by PEEK_LOSS scope_stack: Vec, // active nested scopes; last() = current, empty = top level next_scope_id: u32, // used to generate unique ids for scopes } @@ -486,6 +493,7 @@ impl IdMap { qubit_map: FxHashMap::default(), name_counters: FxHashMap::default(), record_count: 0, + peek_loss_record_ids: FxHashSet::default(), scope_stack: Vec::new(), next_scope_id: 0, } @@ -1297,6 +1305,11 @@ impl<'noise> Compiler<'noise> { "REQUIRE" => self.compile_require(instruction), "NOTLEAKED" => self.compile_notleaked(instruction), + // Miscellaneous + "PEEK_LOSS" => self.broadcast(instruction, |s, q| { + s.op_peek_loss(q); + }), + // Annotations "DETECTOR" | "MPAD" | "OBSERVABLE_INCLUDE" | "QUBIT_COORDS" | "SHIFT_COORDS" | "TICK" => (), @@ -1575,6 +1588,13 @@ impl<'noise> Compiler<'noise> { self.writer.write_qis_call(intrinsic, &[q, r]); } + fn op_peek_loss(&mut self, qubit: u32) { + let q = self.id_map.allocate_qubit(qubit); + let r = self.id_map.allocate_record(); + self.id_map.peek_loss_record_ids.insert(r); + self.writer.write_qis_call("peek_loss", &[q, r]); + } + fn op_2(&mut self, intrinsic: &str, q0: u32, q1: u32) { let q0 = self.id_map.allocate_qubit(q0); let q1 = self.id_map.allocate_qubit(q1); @@ -1644,17 +1664,21 @@ impl<'noise> Compiler<'noise> { return; }; - let mut has_negated_target = false; - for (&(_, negated), target) in record_metadata.iter().zip(&instruction.targets) { + let mut has_error = false; + for (&(result_id, negated), target) in record_metadata.iter().zip(&instruction.targets) { if negated { self.push_error(Error::NegatedTarget { instruction: instruction.name.clone(), span: target.span, }); - has_negated_target = true; + has_error = true; + } + if self.id_map.peek_loss_record_ids.contains(&result_id) { + self.push_error(Error::NotLeakedOnPeekLoss { span: target.span }); + has_error = true; } } - if has_negated_target { + if has_error { return; } From f6a8889f168b624d1fa44ae354e9b45b3e5930fb Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 4 Aug 2026 16:55:55 -0700 Subject: [PATCH 6/6] add tests for peek_loss in stim --- .../compiler/stim_compiler/src/qir/tests.rs | 1 + .../stim_compiler/src/qir/tests/peek_loss.rs | 380 ++++++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 source/compiler/stim_compiler/src/qir/tests/peek_loss.rs diff --git a/source/compiler/stim_compiler/src/qir/tests.rs b/source/compiler/stim_compiler/src/qir/tests.rs index 78c5fe2a81f..4bb7a4f2925 100644 --- a/source/compiler/stim_compiler/src/qir/tests.rs +++ b/source/compiler/stim_compiler/src/qir/tests.rs @@ -9,6 +9,7 @@ mod noise_channels; mod noise_channels_broadcasting; mod pair_measurements; mod pair_measurements_broadcasting; +mod peek_loss; mod repeat; mod select_block; mod single_qubit_gates; diff --git a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs new file mode 100644 index 00000000000..95fa6ff5836 --- /dev/null +++ b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::qir::tests::check; +use expect_test::expect; +use indoc::indoc; + +#[test] +fn peek_loss_single_qubit() { + check( + "PEEK_LOSS 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 1, ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), 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__peek_loss__body(ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" } + 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 peek_loss_broadcasts_over_multiple_qubits() { + check( + "PEEK_LOSS 0 1 2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 2 to ptr)) + call void @__quantum__rt__array_record_output(i64 3, ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 2 to ptr), 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__peek_loss__body(ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="3" } + 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} + "#]], + ); +} + +// temporary, until we start accepting arguments to measurements +#[test] +fn peek_loss_with_args_yields_error() { + check( + "PEEK_LOSS(0.5) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnsupportedArgument + + x unsupported argument in instruction: PEEK_LOSS + ,---- + 1 | PEEK_LOSS(0.5) 0 + : ^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn peek_loss_with_negated_target_yields_error() { + check( + "PEEK_LOSS !0", + &expect![[r#" + Qdk.Stim.Compiler.NegatedTarget + + x target cannot be negated in instruction: PEEK_LOSS + ,---- + 1 | PEEK_LOSS !0 + : ^^ + `---- + "#]], + ); +} + +#[test] +fn peek_loss_referenced_by_classical_control() { + let source = indoc! {" + PEEK_LOSS 0 + CX rec[-1] 1 + "}; + check( + source, + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @classical_control_cx(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__array_record_output(i64 1, ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null) + ret i64 0 + } + + define void @classical_control_cx(ptr %result, ptr %qubit) { + block_cx_entry: + %result_val = call i1 @__quantum__rt__read_result(ptr %result) + br i1 %result_val, label %block_cx_apply, label %block_cx_exit + block_cx_apply: + call void @__quantum__qis__x__body(ptr %qubit) + br label %block_cx_exit + block_cx_exit: + ret void + } + + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__x__body(ptr) + declare i1 @__quantum__rt__read_result(ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__peek_loss__body(ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="1" } + 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 peek_loss_referenced_by_notleaked_in_select_block_yields_error() { + let source = indoc! {" + SELECT { + PEEK_LOSS 0 + NOTLEAKED rec[-1] + } + "}; + check( + source, + &expect![[r#" + Qdk.Stim.Compiler.NotLeakedOnPeekLoss + + x NOTLEAKED cannot reference a record produced by PEEK_LOSS + ,-[3:13] + 2 | PEEK_LOSS 0 + 3 | NOTLEAKED rec[-1] + : ^^^^^^^ + 4 | } + `---- + "#]], + ); +} + +#[test] +fn peek_loss_referenced_by_require_in_select_block() { + let source = indoc! {" + SELECT { + PEEK_LOSS 0 + REQUIRE rec[-1] + } + "}; + check( + source, + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + br label %select_0 + select_0: + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr)) + %l_0 = call i1 @__quantum__rt__read_loss(ptr inttoptr (i64 0 to ptr)) + %r_0 = call i1 @__quantum__rt__read_result(ptr inttoptr (i64 0 to ptr)) + %restart_0 = or i1 %l_0, %r_0 + br i1 %restart_0, label %select_0, label %continue_0 + continue_0: + call void @__quantum__rt__array_record_output(i64 1, ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null) + ret i64 0 + } + + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare i1 @__quantum__rt__read_loss(ptr) + declare i1 @__quantum__rt__read_result(ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__peek_loss__body(ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" } + 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 peek_loss_interleaved_with_measurements() { + let source = indoc! {" + M 0 + PEEK_LOSS 1 + M 2 + "}; + check( + source, + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__m__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__m__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 2 to ptr)) + call void @__quantum__rt__array_record_output(i64 3, ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 2 to ptr), ptr null) + ret i64 0 + } + + declare void @__quantum__qis__m__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__peek_loss__body(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"="3" } + 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 notleaked_errors_on_peek_mixed_with_measurement() { + let source = indoc! {" + SELECT { + PEEK_LOSS 0 + M 1 + NOTLEAKED rec[-1] rec[-2] + } + "}; + check( + source, + &expect![[r#" + Qdk.Stim.Compiler.NotLeakedOnPeekLoss + + x NOTLEAKED cannot reference a record produced by PEEK_LOSS + ,-[4:21] + 3 | M 1 + 4 | NOTLEAKED rec[-1] rec[-2] + : ^^^^^^^ + 5 | } + `---- + "#]], + ); +} + +#[test] +fn require_allows_peek_record_mixed_with_measurement() { + let source = indoc! {" + SELECT { + PEEK_LOSS 0 + M 1 + REQUIRE rec[-1] rec[-2] + } + "}; + check( + source, + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + br label %select_0 + select_0: + call void @__quantum__qis__peek_loss__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__m__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr)) + %l_0 = call i1 @__quantum__rt__read_loss(ptr inttoptr (i64 1 to ptr)) + %r_0 = call i1 @__quantum__rt__read_result(ptr inttoptr (i64 1 to ptr)) + %l_1 = call i1 @__quantum__rt__read_loss(ptr inttoptr (i64 0 to ptr)) + %r_1 = call i1 @__quantum__rt__read_result(ptr inttoptr (i64 0 to ptr)) + %loss_0 = or i1 %l_0, %l_1 + %parity_0 = xor i1 %r_0, %r_1 + %restart_0 = or i1 %loss_0, %parity_0 + br i1 %restart_0, label %select_0, label %continue_0 + continue_0: + call void @__quantum__rt__array_record_output(i64 2, ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null) + ret i64 0 + } + + declare void @__quantum__qis__peek_loss__body(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare i1 @__quantum__rt__read_loss(ptr) + declare i1 @__quantum__rt__read_result(ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__m__body(ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="2" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !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} + "#]], + ); +}