From f7f0f4498d0e3162c6e97603c8e98cdc9c3ff3d9 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 30 Oct 2026 14:40:39 +0100 Subject: [PATCH 01/36] Refactor vihaco-circuit-isa into dialect --- crates/vihaco-circuit-isa/Cargo.toml | 7 +- crates/vihaco-circuit-isa/src/lib.rs | 370 ++++++++++++--------------- 2 files changed, 165 insertions(+), 212 deletions(-) diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index 69bae76df..eccd7e38b 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -10,8 +10,5 @@ description = "Quantum-circuit instruction set for the vihaco virtual machine, u chumsky = "0.10" eyre = "0.6.12" smallvec = "1.15.1" -vihaco = "0.4.1" -vihaco-parser = "0.4.1" - -[package.metadata.cargo-machete] -ignored = ["eyre", "vihaco-parser"] # transitive dependency in vihaco, somehow not handled correctly by machete +vihaco = { version = "0.7.0", features = ["program-image-execution"] } +vihaco-stdlib = { version = "0.7.0", features = ["program-image-execution"] } diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index 1aefa52fc..de2b41e20 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -1,245 +1,201 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -use smallvec::SmallVec; -use vihaco::Message; - -vihaco::component! { - #[derive(Debug, Default)] - pub component Circuit {} - - instruction { - TwoQubitPauliError, - Truncate, - Trace, - X, - Y, - Z, - H, - - #[pattern = "'sqrt_x_adj"] - SqrtXAdj, - - #[pattern = "'sqrt_x"] - SqrtX, - - #[pattern = "'sqrt_y_adj"] - SqrtYAdj, - - #[pattern = "'sqrt_y"] - SqrtY, - - #[pattern = "'s_adj"] - SAdj, - S, - CNOT, - CZ, - TAdj, - T, - RXX, - RYY, - RZZ, - RX, - RY, - RZ, - U3, - Measure, - Reset, - R, - Loss, - CorrelatedLoss, - PauliError, - Depolarize2, - Depolarize, - Leakage, - } -} +use vihaco::dialect; -pub use circuit::{runtime, syntax}; -pub use runtime::Instruction as CircuitInstruction; +#[dialect] +pub mod circuit { + #[instruction] + pub struct TwoQubitPauliError; -pub type CircuitSurfaceInstruction = syntax::Instruction; + #[instruction] + pub struct Truncate; -impl std::fmt::Display for runtime::Instruction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use runtime::Instruction::*; - match self { - TwoQubitPauliError => write!(f, "TwoQubitPauliError"), - Truncate => write!(f, "Truncate"), - Trace => write!(f, "Trace"), + #[instruction] + pub struct Trace; - X => write!(f, "X"), - Y => write!(f, "Y"), - Z => write!(f, "Z"), - H => write!(f, "H"), + #[instruction] + pub struct X; - SqrtXAdj => write!(f, "SqrtXAdj"), + #[instruction] + pub struct Y; - SqrtX => write!(f, "SqrtX"), + #[instruction] + pub struct Z; - SqrtYAdj => write!(f, "SqrtYAdj"), + #[instruction] + pub struct H; - SqrtY => write!(f, "SqrtY"), + #[pattern = "'sqrt_x_adj"] + #[instruction] + pub struct SqrtXAdj; - SAdj => write!(f, "SAdj"), - S => write!(f, "S"), + #[pattern = "'sqrt_x"] + #[instruction] + pub struct SqrtX; - CNOT => write!(f, "CNOT"), - CZ => write!(f, "CZ"), + #[pattern = "'sqrt_y_adj"] + #[instruction] + pub struct SqrtYAdj; - TAdj => write!(f, "TAdj"), - T => write!(f, "T"), + #[pattern = "'sqrt_y"] + #[instruction] + pub struct SqrtY; - RXX => write!(f, "RXX"), - RYY => write!(f, "RYY"), - RZZ => write!(f, "RZZ"), + #[pattern = "'s_adj"] + #[instruction] + pub struct SAdj; - RX => write!(f, "RX"), - RY => write!(f, "RY"), - RZ => write!(f, "RZ"), + #[instruction] + pub struct S; - U3 => write!(f, "U3"), + #[instruction] + pub struct CNOT; - Measure => write!(f, "Measure"), - Reset => write!(f, "Reset"), + #[instruction] + pub struct CZ; - R => write!(f, "R"), + #[pattern = "'t_adj"] + #[instruction] + pub struct TAdj; - Loss => write!(f, "Loss"), - CorrelatedLoss => write!(f, "CorrelatedLoss"), + #[instruction] + pub struct T; - PauliError => write!(f, "PauliError"), - Depolarize2 => write!(f, "Depolarize2"), - Depolarize => write!(f, "Depolarize"), + #[instruction] + pub struct RXX; - Leakage => write!(f, "Leakage"), - } - } -} + #[instruction] + pub struct RYY; -#[derive(Debug, Clone, PartialEq, Message)] -pub enum CircuitMessage { - None, // Truncate (no operand) - Qubit(usize), // X, Y, Z, ... - QubitAndFloat(usize, f64), // RX, depolarize, ... - QubitAndTwoFloats(usize, f64, f64), // R - TwoQubit(usize, usize), // CX, CZ - TwoQubitAndFloat(usize, usize, f64), // RXX, ... - QubitU3(usize, f64, f64, f64), // U3 - QubitAndFloatArr3(usize, [f64; 3]), // PauliError - TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss - TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError - - PauliPatternStr(String), // Trace (resolved Pauli-pattern source) - - // batched instructions - QubitBatch(SmallVec<[usize; 8]>), // X, Y, Z, ... - QubitBatchAndFloat(SmallVec<[usize; 8]>, f64), // RX, depolarize, ... - TwoQubitBatch(SmallVec<[(usize, usize); 8]>), // CX, CZ - TwoQubitBatchAndFloat(SmallVec<[(usize, usize); 8]>, f64), // RXX, ... - QubitBatchU3(SmallVec<[usize; 8]>, f64, f64, f64), // U3 - QubitBatchAndFloatArr3(SmallVec<[usize; 8]>, [f64; 3]), // PauliError - TwoQubitBatchAndFloatArr3(SmallVec<[(usize, usize); 8]>, [f64; 3]), // Correlated loss - TwoQubitBatchAndFloatArr15(SmallVec<[(usize, usize); 8]>, [f64; 15]), // TwoQubitPauliError -} + #[instruction] + pub struct RZZ; + + #[instruction] + pub struct RX; + + #[instruction] + pub struct RY; + + #[instruction] + pub struct RZ; + + #[instruction] + pub struct U3; + + #[instruction] + pub struct Measure; + + #[instruction] + pub struct Reset; + + #[instruction] + pub struct R; + + #[instruction] + pub struct Loss; -#[derive(Debug, Clone)] -pub struct CircuitEffect { - pub inst: CircuitInstruction, - pub msg: CircuitMessage, + #[instruction] + pub struct CorrelatedLoss; + + #[instruction] + pub struct PauliError; + + #[instruction] + pub struct Depolarize2; + + #[instruction] + pub struct Depolarize; + + #[instruction] + pub struct Leakage; } #[cfg(test)] mod tests { - use super::syntax::Instruction::*; - use super::*; - + use super::circuit; use chumsky::Parser as _; use vihaco::Parse; - fn parse(src: &str) -> CircuitSurfaceInstruction { - let src = format!("circuit.{src}"); - CircuitSurfaceInstruction::parser() - .parse(src.as_str()) - .into_result() - .unwrap_or_else(|e| panic!("parse of `{src}` failed: {e:?}")) - } // ─── Parse: tokens are the lowercased variant name ──────────────────── #[test] fn parses_simple_lowercase_tokens() { - assert_eq!(parse("x"), X); - assert_eq!(parse("y"), Y); - assert_eq!(parse("z"), Z); - assert_eq!(parse("h"), H); - assert_eq!(parse("cnot"), CNOT); - assert_eq!(parse("cz"), CZ); - assert_eq!(parse("u3"), U3); - assert_eq!(parse("measure"), Measure); - assert_eq!(parse("reset"), Reset); - assert_eq!(parse("r"), R); - assert_eq!(parse("rx"), RX); - assert_eq!(parse("ry"), RY); - assert_eq!(parse("rz"), RZ); - assert_eq!(parse("rxx"), RXX); - assert_eq!(parse("depolarize2"), Depolarize2); - assert_eq!(parse("depolarize"), Depolarize); - assert_eq!(parse("loss"), Loss); - assert_eq!(parse("leakage"), Leakage); - } - - // ─── Parse: prefix-sensitive disambiguation ─────────────────────────── - // - // These pairs share a prefix, so the declaration order in the enum is - // load-bearing: the longer token must win. These tests pin that contract. - - #[test] - fn parses_t_family_without_prefix_collision() { - // `t` is a prefix of `tadj`, `trace`, `truncate`, and `twoqubitpaulierror`. - assert_eq!(parse("t"), T); - assert_eq!(parse("tadj"), TAdj); - assert_eq!(parse("trace"), Trace); - assert_eq!(parse("truncate"), Truncate); - assert_eq!(parse("twoqubitpaulierror"), TwoQubitPauliError); - } - - #[test] - fn parses_s_family_without_prefix_collision() { - // `s` is a prefix of `s_adj`, `sqrt_x`, `sqrt_y`, etc. - assert_eq!(parse("s"), S); - assert_eq!(parse("s_adj"), SAdj); - assert_eq!(parse("sqrt_x"), SqrtX); - assert_eq!(parse("sqrt_x_adj"), SqrtXAdj); - assert_eq!(parse("sqrt_y"), SqrtY); - assert_eq!(parse("sqrt_y_adj"), SqrtYAdj); - } - - #[test] - fn rejects_unknown_token() { - assert!( - CircuitSurfaceInstruction::parser() - .parse("circuit.nope") - .has_errors() - ); - - for token in ["sadj", "sqrtx", "sqrtxadj", "sqrty", "sqrtyadj"] { - let source = format!("circuit.{token}"); - assert!( - CircuitSurfaceInstruction::parser() - .parse(&source) - .has_errors(), - "legacy compact token should be rejected: {token}" - ); - } - } - - #[test] - fn rejects_pascal_case_token() { - // The parse token is lowercase; the Display form must not parse back. - assert!( - CircuitSurfaceInstruction::parser() - .parse("circuit.CNOT") - .has_errors() + assert_eq!( + circuit::X::parser().parse("x").into_result(), + Ok(circuit::X) + ); + assert_eq!( + circuit::Y::parser().parse("y").into_result(), + Ok(circuit::Y) + ); + assert_eq!( + circuit::Z::parser().parse("z").into_result(), + Ok(circuit::Z) + ); + assert_eq!( + circuit::H::parser().parse("h").into_result(), + Ok(circuit::H) + ); + assert_eq!( + circuit::CNOT::parser().parse("cnot").into_result(), + Ok(circuit::CNOT) + ); + assert_eq!( + circuit::CZ::parser().parse("cz").into_result(), + Ok(circuit::CZ) + ); + assert_eq!( + circuit::U3::parser().parse("u3").into_result(), + Ok(circuit::U3) + ); + assert_eq!( + circuit::Measure::parser().parse("measure").into_result(), + Ok(circuit::Measure) + ); + assert_eq!( + circuit::Reset::parser().parse("reset").into_result(), + Ok(circuit::Reset) + ); + assert_eq!( + circuit::R::parser().parse("r").into_result(), + Ok(circuit::R) + ); + assert_eq!( + circuit::RX::parser().parse("rx").into_result(), + Ok(circuit::RX) + ); + assert_eq!( + circuit::RY::parser().parse("ry").into_result(), + Ok(circuit::RY) + ); + assert_eq!( + circuit::RZ::parser().parse("rz").into_result(), + Ok(circuit::RZ) + ); + assert_eq!( + circuit::RXX::parser().parse("rxx").into_result(), + Ok(circuit::RXX) + ); + assert_eq!( + circuit::Depolarize2::parser() + .parse("depolarize2") + .into_result(), + Ok(circuit::Depolarize2) + ); + assert_eq!( + circuit::Depolarize::parser() + .parse("depolarize") + .into_result(), + Ok(circuit::Depolarize) + ); + assert_eq!( + circuit::Loss::parser().parse("loss").into_result(), + Ok(circuit::Loss) + ); + assert_eq!( + circuit::Leakage::parser().parse("leakage").into_result(), + Ok(circuit::Leakage) ); } } From 9c0b927697127521c77c97b2a09ebb183b2894a2 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 3 Nov 2026 11:05:19 +0100 Subject: [PATCH 02/36] Ignore worktrees --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 35a40df3b..675190926 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .DS_Store .vscode/ .uv/ +.worktrees/ *__pycache__* profile.json.gz From 5b46ed31afc7387ba97161950abf3c1638c40224 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 3 Nov 2026 14:43:44 +0100 Subject: [PATCH 03/36] Implement execute for tableau --- Cargo.lock | 73 +- crates/ppvm-vihaco/Cargo.toml | 6 +- crates/ppvm-vihaco/src/component.rs | 2077 +++++++++++++++------------ crates/ppvm-vihaco/src/lib.rs | 211 ++- 4 files changed, 1328 insertions(+), 1039 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 526c1b310..63158bdeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2408,9 +2408,7 @@ dependencies = [ "smallvec", "vihaco", "vihaco-circuit-isa", - "vihaco-cpu", - "vihaco-parser", - "vihaco-parser-derive", + "vihaco-stdlib", ] [[package]] @@ -3442,9 +3440,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vihaco" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868af4f85b69eaed7e7f7ce8b80dedae1c1f84ab349d99f5062924200d9e48d8" +checksum = "912af9259a5fb3dba120fe8083388f3792b35bb37008f98a25eef1c3597b8ec3" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -3466,22 +3464,23 @@ dependencies = [ [[package]] name = "vihaco-abi" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41eba1bb03d1c13acac5b6bcdc195d410dfcf31fec55dcabc1ba77ef1b72b62" +checksum = "9a6fe13d301df45ac93545cce68166c6afc408f1b1e465e1ceb2d5be39f82930" dependencies = [ "byteorder", "chumsky 0.10.1", "eyre", "smallvec", "vihaco-abi-derive", + "vihaco-parser", ] [[package]] name = "vihaco-abi-derive" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d82820d6174e30131097191fbe81af06151d4000294e7a30017d9ef170b288" +checksum = "3aa2b9efe3e360c8ec0cf9b9414feee479131e130e0041385fdc740d038e3e43" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3491,9 +3490,9 @@ dependencies = [ [[package]] name = "vihaco-bytecode" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2e4dce87f22f042ca709df72b07f3c154e19bb3f3a97f2dc976a0dcc34ee8" +checksum = "bc55ca2deeadfb201ca4764e13e4beca158dfac3c3a8e5a77199d33884ad713c" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -3509,41 +3508,46 @@ dependencies = [ "eyre", "smallvec", "vihaco", - "vihaco-parser", + "vihaco-stdlib", ] [[package]] name = "vihaco-cpu" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9b331dceb7844b2b14e0d71ec3f089cb75219a424d0fb204020381fde0a887" +checksum = "d50058ee0dea40ca48c685704b202cc32f0c9674a5989750fdb87ef38f995f1d" dependencies = [ "chumsky 0.10.1", "codespan", "eyre", "log", - "vihaco", + "vihaco-abi", + "vihaco-module", "vihaco-parser", "vihaco-parser-derive", + "vihaco-runtime", ] [[package]] name = "vihaco-module" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154e6af7c180498f6d91122eef2bc1fb78265d565467e51f5b3c2aa09298b7ad" +checksum = "93cb72f8967f231c48c04396532ed2ac98284db3ee5be0c7867c4e3d6d2a2636" dependencies = [ + "chumsky 0.10.1", "colored", "eyre", "vihaco-abi", "vihaco-bytecode", + "vihaco-parser", + "vihaco-parser-derive", ] [[package]] name = "vihaco-parser" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2817e4f96bebb254211655ecbf55ab31f8ffbd04149820cf747a196483940a" +checksum = "67c7c6ff12e013b826629b49935f5ed1b7995528984bb8f1e77ccc78f370ac33" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -3552,9 +3556,9 @@ dependencies = [ [[package]] name = "vihaco-parser-derive" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49716816ec02d2098acd1ce7f73feb9c95c6cf6c8adab6c9cbc84671a1417fe0" +checksum = "b85e1d714a9af3031cc85cb2b4bd7e2c6d0ad2dee20c78e432182877ec2b16dc" dependencies = [ "chumsky 0.10.1", "eyre", @@ -3566,24 +3570,26 @@ dependencies = [ [[package]] name = "vihaco-runtime" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90d88037beb7373cfd99f270010f4b7b6215cc0d4107cbfd05a042efa5ec5a85" +checksum = "0bb5bd80466633b93da51327cf3ef6cb66e578738f9b815b067e54b196d60544" dependencies = [ "chumsky 0.10.1", "eyre", + "smallvec", "vihaco-abi", "vihaco-bytecode", "vihaco-module", "vihaco-parser", + "vihaco-parser-derive", "vihaco-runtime-derive", ] [[package]] name = "vihaco-runtime-derive" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f2e39337a9b2c34a1d849ad601c6fa3a5da1c2fc5971637670643913b831a7" +checksum = "02a7d0045bf51b27ffc426d0d2627b9cc00ebab0a95363c19c3007d850b257af" dependencies = [ "convert_case", "proc-macro-crate", @@ -3594,23 +3600,32 @@ dependencies = [ [[package]] name = "vihaco-stdlib" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f144e86c79b496099e751fcb2f7ef35fe8d8a5486cd93324d67f76f6d7d3943" +checksum = "52426a39b5d6bf07fa21ad1297250fc1bd7973d482b08782f9689f4acc295881" dependencies = [ + "chumsky 0.10.1", "eyre", + "vihaco-abi", + "vihaco-bytecode", + "vihaco-cpu", + "vihaco-module", + "vihaco-parser", + "vihaco-parser-derive", "vihaco-runtime", ] [[package]] name = "vihaco-syntax" -version = "0.4.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f845dd3cc8b4f6497ce391cd216b57c78419d80475f9a9f3db996b5becbd6e0" +checksum = "cd7065542523d4473d92546c6f1087142945e7bfd1ab3b7f2a163bac968ebf87" dependencies = [ "chumsky 0.10.1", "eyre", + "vihaco-abi", "vihaco-bytecode", + "vihaco-module", "vihaco-parser", ] diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 37d65d8a4..f1eb38ab4 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -22,10 +22,8 @@ num = "0.4.3" rayon = { version = "1.10", optional = true } ppvm-tableau = { version = "0.1.1", path = "../ppvm-tableau" } smallvec = "1.15.1" -vihaco = "0.4.1" -vihaco-cpu = "0.4.1" -vihaco-parser = "0.4.1" -vihaco-parser-derive = "0.4.1" +vihaco = { version = "0.7.0", features = ["program-image-execution"] } +vihaco-stdlib = { version = "0.7.0", features = ["program-image-execution"] } vihaco-circuit-isa = { version = "0.1.1", path = "../vihaco-circuit-isa" } ppvm-pauli-sum = { version = "0.1.1", path = "../ppvm-pauli-sum" } rand = "0.10.3" diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index dd6e777b7..394b552ea 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -14,8 +14,8 @@ use ppvm_pauli_sum::config::fx64hash::Byte8F64; use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; use ppvm_tableau::prelude::*; -use vihaco::{Effects, dispatch, observe}; -use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; +use vihaco::{Effects, Execute, PopFromStack, component, dispatch, observe}; +use vihaco_circuit_isa::circuit; /// Largest qubit count any backend can simulate. The widest size bucket is /// backed by 2048-bit integers (`U2048` / `[u8; 256]`), so every constructor @@ -67,973 +67,1250 @@ macro_rules! batch_pairs_for { }; } -pub struct CircuitExecutor, I: TableauIndex, C: SparseVector> { - pub tab: GeneralizedTableau, -} - -#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -impl CircuitExecutor +#[component] +#[instructions { circuit }] +pub struct CircuitExecutor where T: Config, <::Storage as BitView>::Store: PrimInt, I: TableauIndex + Send + Sync + std::fmt::Debug, C: SparseVector + std::fmt::Debug, { - fn execute( - &mut self, - inst: CircuitInstruction, - msg: CircuitMessage, - ) -> Result> { - self.execute_instruction(&inst, &msg) - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { - use CircuitInstruction::*; - use CircuitMessage::*; - - match (inst, msg) { - // Single-qubit Clifford - (X, &Qubit(addr)) => self.tab.x(addr), - (Y, &Qubit(addr)) => self.tab.y(addr), - (Z, &Qubit(addr)) => self.tab.z(addr), - (H, &Qubit(addr)) => self.tab.h(addr), - (S, &Qubit(addr)) => self.tab.s(addr), - (SAdj, &Qubit(addr)) => self.tab.s_dag(addr), - (SqrtX, &Qubit(addr)) => self.tab.sqrt_x(addr), - (SqrtY, &Qubit(addr)) => self.tab.sqrt_y(addr), - (SqrtXAdj, &Qubit(addr)) => self.tab.sqrt_x_dag(addr), - (SqrtYAdj, &Qubit(addr)) => self.tab.sqrt_y_dag(addr), - - // Controlled gates - (CNOT, &TwoQubit(addr0, addr1)) => self.tab.cnot(addr0, addr1), - (CZ, &TwoQubit(addr0, addr1)) => self.tab.cz(addr0, addr1), - - // T gate - (T, &Qubit(addr)) => self.tab.t(addr), - (TAdj, &Qubit(addr)) => self.tab.t_dag(addr), - - // Single-qubit rotations - (RX, &QubitAndFloat(addr, angle)) => self.tab.rx(addr, angle), - (RY, &QubitAndFloat(addr, angle)) => self.tab.ry(addr, angle), - (RZ, &QubitAndFloat(addr, angle)) => self.tab.rz(addr, angle), - - // Two-qubit rotations - (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rxx(addr0, addr1, angle), - (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.ryy(addr0, addr1, angle), - (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rzz(addr0, addr1, angle), - - // U3 - (U3, &QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), - - // RXY: rotation about an axis in the x/y plane - (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => self.tab.r(addr, axis_angle, theta), - - // Measure & Reset - (Measure, &Qubit(addr)) => { - let outcome: MeasurementOutcome = self.tab.measure(addr).into(); - return Ok(Effects::one(CircuitOutcomeEffect::Measurement( - MeasurementEffect { - measurement_results: smallvec::smallvec![outcome], - }, - ))); - } - (Reset, &Qubit(addr)) => self.tab.reset(addr), - - // Noise - (Depolarize, &QubitAndFloat(addr, p)) => self.tab.depolarize1(addr, p), - (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { - self.tab.depolarize2(addr0, addr1, p) - } - (PauliError, QubitAndFloatArr3(addr0, ps)) => self.tab.pauli_error(*addr0, *ps), - (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { - self.tab.two_qubit_pauli_error(*addr0, *addr1, *ps) - } - - // Loss - (Loss, &QubitAndFloat(addr, p)) => self.tab.loss_channel(addr, p), - (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { - self.tab.correlated_loss_channel(*addr0, *addr1, *ps) - } - (Leakage, &QubitAndTwoFloats(addr, p0, p1)) => self.tab.leakage_channel(addr, p0, p1), - - /* BATCH OPERATIONS START HERE */ - // Batch: dedicated batch methods - (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_many(addrs), - (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_many(addrs), - (SqrtXAdj, QubitBatch(addrs)) => self.tab.sqrt_x_dag_many(addrs), - (SqrtYAdj, QubitBatch(addrs)) => self.tab.sqrt_y_dag_many(addrs), - (H, QubitBatch(addrs)) => self.tab.h_many(addrs), - (CZ, TwoQubitBatch(pairs)) => self.tab.cz_many(pairs), - - // TODO: replace things below by actual batched methods once they are available - // Batch: single-qubit for loops - (X, QubitBatch(addrs)) => self.tab.x_many(addrs), - (Y, QubitBatch(addrs)) => self.tab.y_many(addrs), - (Z, QubitBatch(addrs)) => self.tab.z_many(addrs), - (S, QubitBatch(addrs)) => self.tab.s_many(addrs), - (SAdj, QubitBatch(addrs)) => self.tab.s_dag_many(addrs), - (T, QubitBatch(addrs)) => self.tab.t_many(addrs), - (TAdj, QubitBatch(addrs)) => self.tab.t_dag_many(addrs), - (Reset, QubitBatch(addrs)) => self.tab.reset_many(addrs), - (RX, QubitBatchAndFloat(addrs, angle)) => self.tab.rx_many(addrs, *angle), - (RY, QubitBatchAndFloat(addrs, angle)) => self.tab.ry_many(addrs, *angle), - (RZ, QubitBatchAndFloat(addrs, angle)) => self.tab.rz_many(addrs, *angle), - (Depolarize, QubitBatchAndFloat(addrs, p)) => { - batch_for!(self.tab, depolarize1, addrs, *p) - } - (Loss, QubitBatchAndFloat(addrs, p)) => batch_for!(self.tab, loss_channel, addrs, *p), - (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { - batch_for!(self.tab, pauli_error, addrs, *ps) - } - (U3, QubitBatchU3(addrs, theta, phi, lam)) => { - batch_for!(self.tab, u3, addrs, *theta, *phi, *lam) - } + pub tab: GeneralizedTableau, +} - // Batch: two-qubit for loops - (CNOT, TwoQubitBatch(pairs)) => { - self.tab.cnot_many(pairs); - } - (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { - self.tab.rxx_many(pairs, *angle); - } - (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { - self.tab.ryy_many(pairs, *angle); - } - (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { - self.tab.rzz_many(pairs, *angle); - } - (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { - for &(a, b) in pairs { - self.tab.depolarize2(a, b, *p); - } - } - (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { - for &(a, b) in pairs { - self.tab.two_qubit_pauli_error(a, b, *ps); - } - } - (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { - for &(a, b) in pairs { - self.tab.correlated_loss_channel(a, b, *ps); - } +macro_rules! single_qubit_execute { + ($leaf:ident, $method:ident) => { + impl Execute for CircuitExecutor + where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, + { + type Effect = (); + type Message = PopFromStack; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.tab.$method(msg.0); + Ok(()) } + } + }; +} - // Batch: measure (emits per qubit) - (Measure, QubitBatch(addrs)) => { - let outcomes = self.tab.measure_many(addrs); - return Ok(Effects::one(CircuitOutcomeEffect::Measurement( - MeasurementEffect { - measurement_results: outcomes.iter().map(|&o| o.into()).collect(), - }, - ))); +macro_rules! two_qubit_execute { + ($leaf:ident, $method:ident) => { + impl Execute for CircuitExecutor + where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, + { + type Effect = (); + type Message = PopFromStack<(usize, usize)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.tab.$method(msg.0.0, msg.0.1); + Ok(()) } + } + }; +} - // Truncate is a silent no-op on the Tableau backend — the tableau's - // gate methods already prune via the configured coefficient - // threshold, so there's nothing extra to do here. - (Truncate, None) => {} - - // Trace: parse the resolved pattern and compute Σ_{P matches} ⟨ψ|P|ψ⟩ - // on the tableau state. Asymmetric with the PauliSum semantics by - // design (Decision 9): on the tableau this is a sum of expectations, - // not a coefficient filter. - (Trace, PauliPatternStr(s)) => { - let pat = PauliPattern::parse(s) - .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; - let value = self.tab.trace(&pat); - return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { - value, - }))); +macro_rules! single_qubit_rotation_execute { + ($leaf:ident, $method:ident) => { + impl Execute for CircuitExecutor + where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, + { + type Effect = (); + type Message = PopFromStack<(usize, f64)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.tab.$method(msg.0.0, msg.0.1); + Ok(()) } + } + }; +} - // Fallback - (inst, msg) => { - return Err(eyre!( - "Invalid circuit instruction arguments {:?} for instruction {:?}", - msg, - inst - )); +macro_rules! two_qubit_rotation_execute { + ($leaf:ident, $method:ident) => { + impl Execute for CircuitExecutor + where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, + { + type Effect = (); + type Message = PopFromStack<(usize, usize, f64)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.tab.$method(msg.0.0, msg.0.1, msg.0.2); + Ok(()) } - }; - - Ok(Effects::None) - } + } + }; } -impl vihaco::Reset for CircuitExecutor +single_qubit_execute!(X, x); +single_qubit_execute!(Y, y); +single_qubit_execute!(Z, z); +single_qubit_execute!(T, t); +single_qubit_execute!(S, s); +single_qubit_execute!(TAdj, t_dag); +single_qubit_execute!(SAdj, s_dag); +single_qubit_execute!(SqrtX, sqrt_x); +single_qubit_execute!(H, h); +single_qubit_execute!(SqrtXAdj, sqrt_x_dag); +single_qubit_execute!(SqrtY, sqrt_y); +single_qubit_execute!(SqrtYAdj, sqrt_y_dag); + +two_qubit_execute!(CNOT, cnot); +two_qubit_execute!(CZ, cz); + +single_qubit_rotation_execute!(RX, rx); +single_qubit_rotation_execute!(RY, ry); +single_qubit_rotation_execute!(RZ, rz); + +two_qubit_rotation_execute!(RXX, rxx); +two_qubit_rotation_execute!(RYY, ryy); +two_qubit_rotation_execute!(RZZ, rzz); + +single_qubit_execute!(Reset, reset); +single_qubit_rotation_execute!(Depolarize, depolarize1); +single_qubit_rotation_execute!(Loss, loss_channel); +two_qubit_rotation_execute!(Depolarize2, depolarize2); + +impl Execute for CircuitExecutor where T: Config, <::Storage as BitView>::Store: PrimInt, I: TableauIndex + Send + Sync + std::fmt::Debug, C: SparseVector + std::fmt::Debug, { - fn reset(&mut self) { - self.tab.reset_all(); - } -} + type Effect = (); + type Message = PopFromStack<(usize, f64, f64, f64)>; + type Error = eyre::Report; -/// Shared dispatch body for `PauliSumExecutor` and `LossyPauliSumExecutor`. -/// Every non-loss `CircuitInstruction` lands here. `LossyPauliSumExecutor` -/// matches `Loss` / `CorrelatedLoss` (single + batched) before invoking this -/// macro and never reaches the loss-rejection arm below. -/// -/// `$self` is passed as an `ident` (typically `self`) so the macro's -/// expansion shares hygiene with the surrounding method's `self` parameter. -/// `$inst` / `$msg` are passed the same way; `$backend` is the human-readable -/// backend name baked into error messages. -macro_rules! dispatch_common_paulisum { - ($self:ident, $inst:ident, $msg:ident, $backend:literal) => {{ - use CircuitInstruction::*; - use CircuitMessage::*; - match ($inst, $msg) { - // Single-qubit Clifford - (X, &Qubit(addr)) => $self.state.x(addr), - (Y, &Qubit(addr)) => $self.state.y(addr), - (Z, &Qubit(addr)) => $self.state.z(addr), - (H, &Qubit(addr)) => $self.state.h(addr), - (S, &Qubit(addr)) => $self.state.s(addr), - (SAdj, &Qubit(addr)) => $self.state.s_dag(addr), - (SqrtX, &Qubit(addr)) => $self.state.sqrt_x(addr), - (SqrtY, &Qubit(addr)) => $self.state.sqrt_y(addr), - (SqrtXAdj, &Qubit(addr)) => $self.state.sqrt_x_dag(addr), - (SqrtYAdj, &Qubit(addr)) => $self.state.sqrt_y_dag(addr), - - // Controlled gates - (CNOT, &TwoQubit(addr0, addr1)) => $self.state.cnot(addr0, addr1), - (CZ, &TwoQubit(addr0, addr1)) => $self.state.cz(addr0, addr1), - - // Single-qubit rotations - (RX, &QubitAndFloat(addr, angle)) => $self.state.rx(addr, angle), - (RY, &QubitAndFloat(addr, angle)) => $self.state.ry(addr, angle), - (RZ, &QubitAndFloat(addr, angle)) => $self.state.rz(addr, angle), - - // Two-qubit rotations - (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => { - $self.state.rxx(addr0, addr1, angle) - } - (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => { - $self.state.ryy(addr0, addr1, angle) - } - (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => { - $self.state.rzz(addr0, addr1, angle) - } - - // RXY: rotation about an axis in the x/y plane - (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { - $self.state.r(addr, axis_angle, theta) - } - - // Noise - (Depolarize, &QubitAndFloat(addr, p)) => $self.state.depolarize1(addr, p), - (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { - $self.state.depolarize2(addr0, addr1, p) - } - (PauliError, QubitAndFloatArr3(addr0, ps)) => { - $self.state.pauli_error(*addr0, *ps) - } - (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { - $self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) - } - - // Truncate: pruning per the configured strategy. - (Truncate, None) => $self.state.truncate(), - - // Batched arms: simple for-loop dispatch (no dedicated batch - // methods on PauliSum, unlike GeneralizedTableau). - (X, QubitBatch(addrs)) => $self.state.x_many(addrs), - (Y, QubitBatch(addrs)) => $self.state.y_many(addrs), - (Z, QubitBatch(addrs)) => $self.state.z_many(addrs), - (H, QubitBatch(addrs)) => $self.state.h_many(addrs), - (S, QubitBatch(addrs)) => $self.state.s_many(addrs), - (SAdj, QubitBatch(addrs)) => $self.state.s_dag_many(addrs), - (SqrtX, QubitBatch(addrs)) => $self.state.sqrt_x_many(addrs), - (SqrtY, QubitBatch(addrs)) => $self.state.sqrt_y_many(addrs), - (SqrtXAdj, QubitBatch(addrs)) => $self.state.sqrt_x_dag_many(addrs), - (SqrtYAdj, QubitBatch(addrs)) => $self.state.sqrt_y_dag_many(addrs), - (RX, QubitBatchAndFloat(addrs, angle)) => { - $self.state.rx_many(addrs, *angle) - } - (RY, QubitBatchAndFloat(addrs, angle)) => { - $self.state.ry_many(addrs, *angle) - } - (RZ, QubitBatchAndFloat(addrs, angle)) => { - $self.state.rz_many(addrs, *angle) - } - (Depolarize, QubitBatchAndFloat(addrs, p)) => { - batch_for!($self.state, depolarize1, addrs, *p) - } - (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { - batch_for!($self.state, pauli_error, addrs, *ps) - } - (CNOT, TwoQubitBatch(pairs)) => $self.state.cnot_many(pairs), - (CZ, TwoQubitBatch(pairs)) => $self.state.cz_many(pairs), - (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { - $self.state.rxx_many(pairs, *angle) - } - (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { - $self.state.ryy_many(pairs, *angle) - } - (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { - $self.state.rzz_many(pairs, *angle) - } - (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { - batch_pairs_for!($self.state, depolarize2, pairs, *p) - } - (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { - batch_pairs_for!($self.state, two_qubit_pauli_error, pairs, *ps) - } - - // Not supported on either backend (Decision 11 + Gate Support - // Matrix). Loss / CorrelatedLoss handling differs by backend - // and lives in the caller's impl block, not this macro. - // Leakage is GeneralizedTableau-only (pinned |0⟩/|1⟩), not a - // LossyPauliSum qutrit |L⟩ channel. - (Measure | Reset | Leakage, _) => { - return Err(eyre!("{} is not supported on the {} backend", $inst, $backend)); - } - - (T, Qubit(addr)) => $self.state.rz(*addr, std::f64::consts::PI / 8.0), - (T, QubitBatch(addrs)) => $self.state.rz_many(addrs, std::f64::consts::PI / 8.0), - (TAdj, Qubit(addr)) => $self.state.rz(*addr, -std::f64::consts::PI / 8.0), - (TAdj, QubitBatch(addrs)) => $self.state.rz_many(addrs, -std::f64::consts::PI / 8.0), - - // U3(θ, φ, λ) = RZ(φ)·RY(θ)·RZ(λ), matching the tableau / Python - // `u3` (which applies rz(λ); ry(θ); rz(φ)). PauliSum is the - // Heisenberg picture, so the sub-rotations run in reversed order - // with the same angles: rz(φ); ry(θ); rz(λ). - (U3, &QubitU3(addr, theta, phi, lam)) => { - $self.state.rz(addr, phi); - $self.state.ry(addr, theta); - $self.state.rz(addr, lam); - } - - (U3, QubitBatchU3(addrs, theta, phi, lam)) => { - $self.state.rz_many(addrs, *phi); - $self.state.ry_many(addrs, *theta); - $self.state.rz_many(addrs, *lam); - } - - // Trace: parse the resolved pattern string and compute the - // trace. Per plan Decision 9, parsing happens on every - // execution; no module-load caching. - (Trace, PauliPatternStr(s)) => { - let pat = PauliPattern::parse(s) - .map_err(|e| eyre!("invalid Pauli pattern `{}`: {:?}", s, e))?; - let value = $self.state.trace(&pat); - return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { - value, - }))); - } - - // Fallback (mismatched shapes, etc.) - (inst, msg) => { - return Err(eyre!( - "Invalid circuit instruction arguments {:?} for instruction {:?} on the {} backend", - msg, - inst, - $backend - )); - } - }; - Ok(Effects::None) - }}; -} - -/// PauliSum-backed executor (Heisenberg picture). Holds a `PauliSum` and -/// answers the same `CircuitInstruction` vocabulary as `CircuitExecutor`, -/// but without measurement / reset / loss support. -pub struct PauliSumExecutor> { - pub state: PauliSum, - /// Snapshot of the seeded observable, restored by `reset`. - initial: PauliSum, + fn execute(&mut self, _: &circuit::U3, msg: Self::Message) -> eyre::Result<()> { + self.tab.u3(msg.0.0, msg.0.1, msg.0.2, msg.0.3); + Ok(()) + } } -#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -impl PauliSumExecutor +impl Execute for CircuitExecutor where T: Config, - for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, { - fn execute( - &mut self, - inst: CircuitInstruction, - msg: CircuitMessage, - ) -> Result> { - self.execute_instruction(&inst, &msg) - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { - use CircuitInstruction::*; - - if matches!(inst, Loss | CorrelatedLoss) { - return Err(eyre!( - "{inst} is not supported on the PauliSum backend; use the LossyPauliSum backend instead" - )); - } + type Effect = (); + type Message = PopFromStack<(usize, f64, f64)>; + type Error = eyre::Report; - dispatch_common_paulisum!(self, inst, msg, "PauliSum") + fn execute(&mut self, _: &circuit::R, msg: Self::Message) -> eyre::Result<()> { + self.tab.r(msg.0.0, msg.0.1, msg.0.2); + Ok(()) } } -impl vihaco::Reset for PauliSumExecutor +impl Execute for CircuitExecutor where T: Config, - PauliSum: Clone, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, { - fn reset(&mut self) { - self.state = self.initial.clone(); - } -} + type Effect = (); + type Message = PopFromStack<(usize, f64, f64)>; + type Error = eyre::Report; -/// LossyPauliSum-backed executor. Same dispatch as `PauliSumExecutor` plus -/// `Loss` / `CorrelatedLoss` channels. The concrete `T` used by the -/// enclosing `Circuit::LossyPauliSum` variant is a `Config` whose -/// `PauliWordType` is `LossyPauliWord` (see `LossyPauliSumConfig`). -pub struct LossyPauliSumExecutor> { - pub state: PauliSum, - /// Snapshot of the seeded observable, restored by `reset`. - initial: PauliSum, + fn execute(&mut self, _: &circuit::Leakage, msg: Self::Message) -> eyre::Result<()> { + self.tab.leakage_channel(msg.0.0, msg.0.1, msg.0.2); + Ok(()) + } } -#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -impl LossyPauliSumExecutor +impl Execute for CircuitExecutor where T: Config, - for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, { - fn execute( - &mut self, - inst: CircuitInstruction, - msg: CircuitMessage, - ) -> Result> { - self.execute_instruction(&inst, &msg) - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { - use CircuitInstruction::*; - use CircuitMessage::*; - - // Loss / CorrelatedLoss are the only instructions that differ from - // PauliSum; handle them here then delegate everything else to the - // shared dispatch. - match (inst, msg) { - (Loss, &QubitAndFloat(addr, p)) => { - self.state.loss_channel(addr, p); - return Ok(Effects::None); - } - (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { - self.state.correlated_loss_channel(*addr0, *addr1, *ps); - return Ok(Effects::None); - } - (Loss, QubitBatchAndFloat(addrs, p)) => { - batch_for!(self.state, loss_channel, addrs, *p); - return Ok(Effects::None); - } - (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { - batch_pairs_for!(self.state, correlated_loss_channel, pairs, *ps); - return Ok(Effects::None); - } - _ => {} - } + type Effect = (); + type Message = PopFromStack<(usize, [f64; 3])>; + type Error = eyre::Report; - dispatch_common_paulisum!(self, inst, msg, "LossyPauliSum") + fn execute(&mut self, _: &circuit::PauliError, msg: Self::Message) -> eyre::Result<()> { + self.tab.pauli_error(msg.0.0, msg.0.1); + Ok(()) } } -impl vihaco::Reset for LossyPauliSumExecutor +impl Execute for CircuitExecutor where T: Config, - PauliSum: Clone, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, { - fn reset(&mut self) { - self.state = self.initial.clone(); - } -} - -/// Tableau-backed inner enum (Schrödinger picture). Carries the six -/// size-bucketed `CircuitExecutor` variants; bucket is picked from `n_qubits`. -pub enum TableauCircuit { - Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>), - Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>), - Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>), - Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>), - Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>), - Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>), -} - -impl TableauCircuit { - pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Result { - if n_qubits <= 64 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits64(CircuitExecutor { tab })) - } else if n_qubits <= 128 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits128(CircuitExecutor { tab })) - } else if n_qubits <= 256 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits256(CircuitExecutor { tab })) - } else if n_qubits <= 512 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits512(CircuitExecutor { tab })) - } else if n_qubits <= 1024 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits1024(CircuitExecutor { tab })) - } else if n_qubits <= MAX_QUBITS { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits2048(CircuitExecutor { tab })) - } else { - Err(eyre!( - "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" - )) - } - } - - /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a - /// shot is reproducible. - pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Result { - macro_rules! seeded { - ($variant:ident) => {{ - let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); - Ok(Self::$variant(CircuitExecutor { tab })) - }}; - } - if n_qubits <= 64 { - seeded!(Bits64) - } else if n_qubits <= 128 { - seeded!(Bits128) - } else if n_qubits <= 256 { - seeded!(Bits256) - } else if n_qubits <= 512 { - seeded!(Bits512) - } else if n_qubits <= 1024 { - seeded!(Bits1024) - } else if n_qubits <= MAX_QUBITS { - seeded!(Bits2048) - } else { - Err(eyre!( - "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" - )) - } - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { - match self { - Self::Bits64(ex) => ex.execute_instruction(inst, msg), - Self::Bits128(ex) => ex.execute_instruction(inst, msg), - Self::Bits256(ex) => ex.execute_instruction(inst, msg), - Self::Bits512(ex) => ex.execute_instruction(inst, msg), - Self::Bits1024(ex) => ex.execute_instruction(inst, msg), - Self::Bits2048(ex) => ex.execute_instruction(inst, msg), - } - } - - pub fn state_string(&self) -> String { - match self { - Self::Bits64(ex) => ex.tab.to_string(), - Self::Bits128(ex) => ex.tab.to_string(), - Self::Bits256(ex) => ex.tab.to_string(), - Self::Bits512(ex) => ex.tab.to_string(), - Self::Bits1024(ex) => ex.tab.to_string(), - Self::Bits2048(ex) => ex.tab.to_string(), - } - } -} + type Effect = (); + type Message = PopFromStack<(usize, usize, [f64; 15])>; + type Error = eyre::Report; -impl vihaco::Reset for TableauCircuit { - fn reset(&mut self) { - match self { - Self::Bits64(ex) => ex.reset(), - Self::Bits128(ex) => ex.reset(), - Self::Bits256(ex) => ex.reset(), - Self::Bits512(ex) => ex.reset(), - Self::Bits1024(ex) => ex.reset(), - Self::Bits2048(ex) => ex.reset(), - }; + fn execute(&mut self, _: &circuit::TwoQubitPauliError, msg: Self::Message) -> eyre::Result<()> { + self.tab.two_qubit_pauli_error(msg.0.0, msg.0.1, msg.0.2); + Ok(()) } } -/// PauliSum-backed inner enum (Heisenberg picture). Per Decision 7 of the plan, -/// the size buckets carry `[u8; N]`-storage `ByteFxHashF64` configs (N = 8, 16, -/// …, 256) rather than the tableau's `[u64; N]` configs; bucket labels match -/// the semantic qubit count (`Bits64` = 64 qubits) so the outer enum's dispatch -/// is uniform across backends. -pub enum PauliSumCircuit { - Bits64(PauliSumExecutor>), - Bits128(PauliSumExecutor>), - Bits256(PauliSumExecutor>), - Bits512(PauliSumExecutor>), - Bits1024(PauliSumExecutor>), - Bits2048(PauliSumExecutor>), -} - -impl PauliSumCircuit { - /// Build a PauliSum-backed circuit, seeding the state with every term: - /// `for (word, coef) in terms { state += (word, coef); }`. Words must - /// already be validated against `info.n_qubits` by the caller. - pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - macro_rules! build { - ($variant:ident, $N:literal) => {{ - let mut state = PauliSum::>::builder() - .n_qubits(info.n_qubits) - .strategy(paulisum_strategy(info)) - .build(); - for (word, coef) in terms { - state += (word.as_str(), *coef); - } - let initial = state.clone(); - Ok(Self::$variant(PauliSumExecutor { state, initial })) - }}; - } - if info.n_qubits <= 64 { - build!(Bits64, 8) - } else if info.n_qubits <= 128 { - build!(Bits128, 16) - } else if info.n_qubits <= 256 { - build!(Bits256, 32) - } else if info.n_qubits <= 512 { - build!(Bits512, 64) - } else if info.n_qubits <= 1024 { - build!(Bits1024, 128) - } else if info.n_qubits <= MAX_QUBITS { - build!(Bits2048, 256) - } else { - Err(eyre!( - "cannot simulate {} qubits: maximum is {MAX_QUBITS}", - info.n_qubits - )) - } - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { - match self { - Self::Bits64(ex) => ex.execute_instruction(inst, msg), - Self::Bits128(ex) => ex.execute_instruction(inst, msg), - Self::Bits256(ex) => ex.execute_instruction(inst, msg), - Self::Bits512(ex) => ex.execute_instruction(inst, msg), - Self::Bits1024(ex) => ex.execute_instruction(inst, msg), - Self::Bits2048(ex) => ex.execute_instruction(inst, msg), - } - } +impl Execute for CircuitExecutor +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + type Effect = (); + type Message = PopFromStack<(usize, usize, [f64; 3])>; + type Error = eyre::Report; - pub fn state_string(&self) -> String { - match self { - Self::Bits64(ex) => ex.state.to_string(), - Self::Bits128(ex) => ex.state.to_string(), - Self::Bits256(ex) => ex.state.to_string(), - Self::Bits512(ex) => ex.state.to_string(), - Self::Bits1024(ex) => ex.state.to_string(), - Self::Bits2048(ex) => ex.state.to_string(), - } + fn execute(&mut self, _: &circuit::CorrelatedLoss, msg: Self::Message) -> eyre::Result<()> { + self.tab.correlated_loss_channel(msg.0.0, msg.0.1, msg.0.2); + Ok(()) } } -impl vihaco::Reset for PauliSumCircuit { - fn reset(&mut self) { - match self { - Self::Bits64(ex) => ex.reset(), - Self::Bits128(ex) => ex.reset(), - Self::Bits256(ex) => ex.reset(), - Self::Bits512(ex) => ex.reset(), - Self::Bits1024(ex) => ex.reset(), - Self::Bits2048(ex) => ex.reset(), - }; +impl Execute for CircuitExecutor +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + type Effect = Effects; + type Message = PopFromStack; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::Measure, msg: Self::Message) -> eyre::Result { + let outcome: MeasurementOutcome = self.tab.measure(msg.0).into(); + Ok(Effects::one(CircuitOutcomeEffect::Measurement( + MeasurementEffect { + measurement_results: smallvec::smallvec![outcome], + }, + ))) } } -/// LossyPauliSum-backed inner enum. Identical shape to [`PauliSumCircuit`] -/// but with `LossyPauliWord`-keyed configs so loss-channel methods dispatch. -pub enum LossyPauliSumCircuit { - Bits64(LossyPauliSumExecutor>), - Bits128(LossyPauliSumExecutor>), - Bits256(LossyPauliSumExecutor>), - Bits512(LossyPauliSumExecutor>), - Bits1024(LossyPauliSumExecutor>), - Bits2048(LossyPauliSumExecutor>), -} - -impl LossyPauliSumCircuit { - /// Build a LossyPauliSum-backed circuit, seeding every term via - /// `state += (word, coef)`. Words must already be validated against - /// `info.n_qubits` by the caller. - pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - macro_rules! build { - ($variant:ident, $N:literal) => {{ - let mut state = PauliSum::>::builder() - .n_qubits(info.n_qubits) - .strategy(paulisum_strategy(info)) - .build(); - for (word, coef) in terms { - state += (word.as_str(), *coef); - } - let initial = state.clone(); - Ok(Self::$variant(LossyPauliSumExecutor { state, initial })) - }}; - } - if info.n_qubits <= 64 { - build!(Bits64, 8) - } else if info.n_qubits <= 128 { - build!(Bits128, 16) - } else if info.n_qubits <= 256 { - build!(Bits256, 32) - } else if info.n_qubits <= 512 { - build!(Bits512, 64) - } else if info.n_qubits <= 1024 { - build!(Bits1024, 128) - } else if info.n_qubits <= MAX_QUBITS { - build!(Bits2048, 256) - } else { - Err(eyre!( - "cannot simulate {} qubits: maximum is {MAX_QUBITS}", - info.n_qubits - )) - } - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { - match self { - Self::Bits64(ex) => ex.execute_instruction(inst, msg), - Self::Bits128(ex) => ex.execute_instruction(inst, msg), - Self::Bits256(ex) => ex.execute_instruction(inst, msg), - Self::Bits512(ex) => ex.execute_instruction(inst, msg), - Self::Bits1024(ex) => ex.execute_instruction(inst, msg), - Self::Bits2048(ex) => ex.execute_instruction(inst, msg), - } - } - - pub fn state_string(&self) -> String { - match self { - Self::Bits64(ex) => ex.state.to_string(), - Self::Bits128(ex) => ex.state.to_string(), - Self::Bits256(ex) => ex.state.to_string(), - Self::Bits512(ex) => ex.state.to_string(), - Self::Bits1024(ex) => ex.state.to_string(), - Self::Bits2048(ex) => ex.state.to_string(), - } - } -} +// Truncate is a no-op on the tableau: gate methods already prune via the threshold. +impl Execute for CircuitExecutor +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + type Effect = (); + type Message = vihaco::NoMessage; + type Error = eyre::Report; -impl vihaco::Reset for LossyPauliSumCircuit { - fn reset(&mut self) { - match self { - Self::Bits64(ex) => ex.reset(), - Self::Bits128(ex) => ex.reset(), - Self::Bits256(ex) => ex.reset(), - Self::Bits512(ex) => ex.reset(), - Self::Bits1024(ex) => ex.reset(), - Self::Bits2048(ex) => ex.reset(), - }; + fn execute(&mut self, _: &circuit::Truncate, _: Self::Message) -> eyre::Result<()> { + Ok(()) } } -/// Outer `Circuit` enum: backend selector. Picks one of the three inner enums -/// based on `info.backend` at construction time; from there, every per-step -/// call routes outer → inner → executor. -pub enum Circuit { - Tableau(TableauCircuit), - PauliSum(PauliSumCircuit), - LossyPauliSum(LossyPauliSumCircuit), -} - -impl vihaco::HasInstructionSet for Circuit { - type Runtime = vihaco_circuit_isa::CircuitInstruction; - type Syntax = vihaco_circuit_isa::CircuitSurfaceInstruction; -} - -#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -impl Circuit { - /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and - /// `coefficient_threshold` from `info`; no observable required. - pub fn tableau(info: &PPVMDeviceInfo) -> Result { - Ok(Self::Tableau(TableauCircuit::new( - info.n_qubits, - info.coefficient_threshold, - )?)) - } - - /// Same as [`Circuit::tableau`], but seed the tableau RNG deterministically - /// so a shot is reproducible. - pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { - Ok(Self::Tableau(TableauCircuit::new_with_seed( - info.n_qubits, - info.coefficient_threshold, - seed, - )?)) - } - - /// Build a PauliSum-backed circuit, seeding the state with every term in - /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the - /// caller is responsible for having parsed/validated the words against - /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). - pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - Ok(Self::PauliSum(PauliSumCircuit::new(info, terms)?)) - } - - /// Build a LossyPauliSum-backed circuit. Same contract as - /// [`Circuit::paulisum`]. - pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - Ok(Self::LossyPauliSum(LossyPauliSumCircuit::new(info, terms)?)) - } - - fn execute( - &mut self, - inst: CircuitInstruction, - msg: CircuitMessage, - ) -> Result> { - self.execute_instruction(&inst, &msg) - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { - match self { - Self::Tableau(c) => c.execute_instruction(inst, msg), - Self::PauliSum(c) => c.execute_instruction(inst, msg), - Self::LossyPauliSum(c) => c.execute_instruction(inst, msg), - } - } +impl Execute for CircuitExecutor +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + type Effect = (); + type Message = vihaco::NoMessage; + type Error = eyre::Report; - /// Render the current state. Used by the REPL's `show` command. - pub fn state_string(&self) -> String { - match self { - Self::Tableau(c) => c.state_string(), - Self::PauliSum(c) => c.state_string(), - Self::LossyPauliSum(c) => c.state_string(), - } + fn execute(&mut self, _: &circuit::Trace, _: Self::Message) -> eyre::Result<()> { + unimplemented!("TODO: needs a custom message"); } } -#[observe(CircuitEffect, effect=CircuitOutcomeEffect)] -impl Circuit { - fn observe_circuit_effect( - &mut self, - effect: &CircuitEffect, - ) -> Result> { - self.execute_instruction(&effect.inst, &effect.msg) - } -} +// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +// impl CircuitExecutor +// where +// T: Config, +// <::Storage as BitView>::Store: PrimInt, +// I: TableauIndex + Send + Sync + std::fmt::Debug, +// C: SparseVector + std::fmt::Debug, +// { +// fn execute( +// &mut self, +// inst: CircuitInstruction, +// msg: CircuitMessage, +// ) -> Result> { +// self.execute_instruction(&inst, &msg) +// } + +// fn execute_instruction( +// &mut self, +// inst: &CircuitInstruction, +// msg: &CircuitMessage, +// ) -> Result> { +// use CircuitInstruction::*; +// use CircuitMessage::*; + +// match (inst, msg) { +// // Single-qubit Clifford +// (X, &Qubit(addr)) => self.tab.x(addr), +// (Y, &Qubit(addr)) => self.tab.y(addr), +// (Z, &Qubit(addr)) => self.tab.z(addr), +// (H, &Qubit(addr)) => self.tab.h(addr), +// (S, &Qubit(addr)) => self.tab.s(addr), +// (SAdj, &Qubit(addr)) => self.tab.s_dag(addr), +// (SqrtX, &Qubit(addr)) => self.tab.sqrt_x(addr), +// (SqrtY, &Qubit(addr)) => self.tab.sqrt_y(addr), +// (SqrtXAdj, &Qubit(addr)) => self.tab.sqrt_x_dag(addr), +// (SqrtYAdj, &Qubit(addr)) => self.tab.sqrt_y_dag(addr), + +// // Controlled gates +// (CNOT, &TwoQubit(addr0, addr1)) => self.tab.cnot(addr0, addr1), +// (CZ, &TwoQubit(addr0, addr1)) => self.tab.cz(addr0, addr1), + +// // T gate +// (T, &Qubit(addr)) => self.tab.t(addr), +// (TAdj, &Qubit(addr)) => self.tab.t_dag(addr), + +// // Single-qubit rotations +// (RX, &QubitAndFloat(addr, angle)) => self.tab.rx(addr, angle), +// (RY, &QubitAndFloat(addr, angle)) => self.tab.ry(addr, angle), +// (RZ, &QubitAndFloat(addr, angle)) => self.tab.rz(addr, angle), + +// // Two-qubit rotations +// (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rxx(addr0, addr1, angle), +// (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.ryy(addr0, addr1, angle), +// (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rzz(addr0, addr1, angle), + +// // U3 +// (U3, &QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), + +// // RXY: rotation about an axis in the x/y plane +// (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => self.tab.r(addr, axis_angle, theta), + +// // Measure & Reset +// (Measure, &Qubit(addr)) => { +// let outcome: MeasurementOutcome = self.tab.measure(addr).into(); +// return Ok(Effects::one(CircuitOutcomeEffect::Measurement( +// MeasurementEffect { +// measurement_results: smallvec::smallvec![outcome], +// }, +// ))); +// } +// (Reset, &Qubit(addr)) => self.tab.reset(addr), + +// // Noise +// (Depolarize, &QubitAndFloat(addr, p)) => self.tab.depolarize1(addr, p), +// (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { +// self.tab.depolarize2(addr0, addr1, p) +// } +// (PauliError, QubitAndFloatArr3(addr0, ps)) => self.tab.pauli_error(*addr0, *ps), +// (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { +// self.tab.two_qubit_pauli_error(*addr0, *addr1, *ps) +// } + +// // Loss +// (Loss, &QubitAndFloat(addr, p)) => self.tab.loss_channel(addr, p), +// (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { +// self.tab.correlated_loss_channel(*addr0, *addr1, *ps) +// } +// (Leakage, &QubitAndTwoFloats(addr, p0, p1)) => self.tab.leakage_channel(addr, p0, p1), + +// /* BATCH OPERATIONS START HERE */ +// // Batch: dedicated batch methods +// (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_many(addrs), +// (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_many(addrs), +// (SqrtXAdj, QubitBatch(addrs)) => self.tab.sqrt_x_dag_many(addrs), +// (SqrtYAdj, QubitBatch(addrs)) => self.tab.sqrt_y_dag_many(addrs), +// (H, QubitBatch(addrs)) => self.tab.h_many(addrs), +// (CZ, TwoQubitBatch(pairs)) => self.tab.cz_many(pairs), + +// // TODO: replace things below by actual batched methods once they are available +// // Batch: single-qubit for loops +// (X, QubitBatch(addrs)) => self.tab.x_many(addrs), +// (Y, QubitBatch(addrs)) => self.tab.y_many(addrs), +// (Z, QubitBatch(addrs)) => self.tab.z_many(addrs), +// (S, QubitBatch(addrs)) => self.tab.s_many(addrs), +// (SAdj, QubitBatch(addrs)) => self.tab.s_dag_many(addrs), +// (T, QubitBatch(addrs)) => self.tab.t_many(addrs), +// (TAdj, QubitBatch(addrs)) => self.tab.t_dag_many(addrs), +// (Reset, QubitBatch(addrs)) => self.tab.reset_many(addrs), +// (RX, QubitBatchAndFloat(addrs, angle)) => self.tab.rx_many(addrs, *angle), +// (RY, QubitBatchAndFloat(addrs, angle)) => self.tab.ry_many(addrs, *angle), +// (RZ, QubitBatchAndFloat(addrs, angle)) => self.tab.rz_many(addrs, *angle), +// (Depolarize, QubitBatchAndFloat(addrs, p)) => { +// batch_for!(self.tab, depolarize1, addrs, *p) +// } +// (Loss, QubitBatchAndFloat(addrs, p)) => batch_for!(self.tab, loss_channel, addrs, *p), +// (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { +// batch_for!(self.tab, pauli_error, addrs, *ps) +// } +// (U3, QubitBatchU3(addrs, theta, phi, lam)) => { +// batch_for!(self.tab, u3, addrs, *theta, *phi, *lam) +// } + +// // Batch: two-qubit for loops +// (CNOT, TwoQubitBatch(pairs)) => { +// self.tab.cnot_many(pairs); +// } +// (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { +// self.tab.rxx_many(pairs, *angle); +// } +// (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { +// self.tab.ryy_many(pairs, *angle); +// } +// (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { +// self.tab.rzz_many(pairs, *angle); +// } +// (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { +// for &(a, b) in pairs { +// self.tab.depolarize2(a, b, *p); +// } +// } +// (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { +// for &(a, b) in pairs { +// self.tab.two_qubit_pauli_error(a, b, *ps); +// } +// } +// (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { +// for &(a, b) in pairs { +// self.tab.correlated_loss_channel(a, b, *ps); +// } +// } + +// // Batch: measure (emits per qubit) +// (Measure, QubitBatch(addrs)) => { +// let outcomes = self.tab.measure_many(addrs); +// return Ok(Effects::one(CircuitOutcomeEffect::Measurement( +// MeasurementEffect { +// measurement_results: outcomes.iter().map(|&o| o.into()).collect(), +// }, +// ))); +// } + +// // Truncate is a silent no-op on the Tableau backend — the tableau's +// // gate methods already prune via the configured coefficient +// // threshold, so there's nothing extra to do here. +// (Truncate, None) => {} + +// // Trace: parse the resolved pattern and compute Σ_{P matches} ⟨ψ|P|ψ⟩ +// // on the tableau state. Asymmetric with the PauliSum semantics by +// // design (Decision 9): on the tableau this is a sum of expectations, +// // not a coefficient filter. +// (Trace, PauliPatternStr(s)) => { +// let pat = PauliPattern::parse(s) +// .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; +// let value = self.tab.trace(&pat); +// return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { +// value, +// }))); +// } + +// // Fallback +// (inst, msg) => { +// return Err(eyre!( +// "Invalid circuit instruction arguments {:?} for instruction {:?}", +// msg, +// inst +// )); +// } +// }; + +// Ok(Effects::None) +// } +// } -impl vihaco::Reset for Circuit { +impl vihaco::Reset for CircuitExecutor +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ fn reset(&mut self) { - match self { - Self::Tableau(c) => c.reset(), - Self::PauliSum(c) => c.reset(), - Self::LossyPauliSum(c) => c.reset(), - }; - } -} - -impl Default for Circuit { - fn default() -> Self { - // Default backend is Tableau with 0 qubits, which always fits the - // smallest bucket, so construction here is infallible. - Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") + self.tab.reset_all(); } } -#[cfg(test)] -mod tests { - use super::*; - - fn info(n_qubits: usize) -> PPVMDeviceInfo { - PPVMDeviceInfo { - n_qubits, - ..Default::default() - } - } - - /// Dispatch a single-qubit `Measure` and read the outcome out of the - /// returned effect. - fn measure(circuit: &mut Circuit, addr: usize) -> MeasurementOutcome { - let effects = circuit - .execute_instruction(&CircuitInstruction::Measure, &CircuitMessage::Qubit(addr)) - .unwrap(); - match effects.into_iter().next() { - Some(CircuitOutcomeEffect::Measurement(m)) => m.measurement_results[0], - other => panic!("expected a measurement effect, got {other:?}"), - } - } - - /// Smoke test: a single-qubit gate dispatches to the tableau backend and - /// flips the qubit. - #[test] - fn tableau_backend_x_flips_qubit() { - let mut circuit = Circuit::tableau(&info(1)).unwrap(); - circuit - .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) - .unwrap(); - assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); - } - - /// Smoke test: a two-qubit gate dispatches correctly — `X(0); CNOT(0, 1)` - /// leaves both qubits in |1⟩. - #[test] - fn tableau_backend_cnot_propagates_flip() { - let mut circuit = Circuit::tableau(&info(2)).unwrap(); - circuit - .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) - .unwrap(); - circuit - .execute_instruction(&CircuitInstruction::CNOT, &CircuitMessage::TwoQubit(0, 1)) - .unwrap(); - assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); - assert_eq!(measure(&mut circuit, 1), MeasurementOutcome::One); - } - - // ─── Construction rejects more qubits than the backend ceiling ──────── - // - // The widest executor bucket is 2048 qubits (U2048 / `[u8; 256]`); beyond - // that there is no backing width, so the constructors return an error - // rather than panicking — a panic would tear down the TUI that drives this. - - #[test] - fn tableau_rejects_more_than_2048_qubits() { - assert!(Circuit::tableau(&info(MAX_QUBITS + 1)).is_err()); - } - - #[test] - fn tableau_with_seed_rejects_more_than_2048_qubits() { - assert!(Circuit::tableau_with_seed(&info(MAX_QUBITS + 1), 0).is_err()); - } - - #[test] - fn paulisum_rejects_more_than_2048_qubits() { - assert!(Circuit::paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); - } - - #[test] - fn lossy_paulisum_rejects_more_than_2048_qubits() { - assert!(Circuit::lossy_paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); - } - - #[test] - fn constructs_at_the_2048_qubit_boundary() { - // 2048 is the last valid bucket; it must still succeed. - assert!(Circuit::tableau(&info(MAX_QUBITS)).is_ok()); - } -} +// /// Shared dispatch body for `PauliSumExecutor` and `LossyPauliSumExecutor`. +// /// Every non-loss `CircuitInstruction` lands here. `LossyPauliSumExecutor` +// /// matches `Loss` / `CorrelatedLoss` (single + batched) before invoking this +// /// macro and never reaches the loss-rejection arm below. +// /// +// /// `$self` is passed as an `ident` (typically `self`) so the macro's +// /// expansion shares hygiene with the surrounding method's `self` parameter. +// /// `$inst` / `$msg` are passed the same way; `$backend` is the human-readable +// /// backend name baked into error messages. +// macro_rules! dispatch_common_paulisum { +// ($self:ident, $inst:ident, $msg:ident, $backend:literal) => {{ +// use CircuitInstruction::*; +// use CircuitMessage::*; +// match ($inst, $msg) { +// // Single-qubit Clifford +// (X, &Qubit(addr)) => $self.state.x(addr), +// (Y, &Qubit(addr)) => $self.state.y(addr), +// (Z, &Qubit(addr)) => $self.state.z(addr), +// (H, &Qubit(addr)) => $self.state.h(addr), +// (S, &Qubit(addr)) => $self.state.s(addr), +// (SAdj, &Qubit(addr)) => $self.state.s_dag(addr), +// (SqrtX, &Qubit(addr)) => $self.state.sqrt_x(addr), +// (SqrtY, &Qubit(addr)) => $self.state.sqrt_y(addr), +// (SqrtXAdj, &Qubit(addr)) => $self.state.sqrt_x_dag(addr), +// (SqrtYAdj, &Qubit(addr)) => $self.state.sqrt_y_dag(addr), + +// // Controlled gates +// (CNOT, &TwoQubit(addr0, addr1)) => $self.state.cnot(addr0, addr1), +// (CZ, &TwoQubit(addr0, addr1)) => $self.state.cz(addr0, addr1), + +// // Single-qubit rotations +// (RX, &QubitAndFloat(addr, angle)) => $self.state.rx(addr, angle), +// (RY, &QubitAndFloat(addr, angle)) => $self.state.ry(addr, angle), +// (RZ, &QubitAndFloat(addr, angle)) => $self.state.rz(addr, angle), + +// // Two-qubit rotations +// (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => { +// $self.state.rxx(addr0, addr1, angle) +// } +// (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => { +// $self.state.ryy(addr0, addr1, angle) +// } +// (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => { +// $self.state.rzz(addr0, addr1, angle) +// } + +// // RXY: rotation about an axis in the x/y plane +// (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { +// $self.state.r(addr, axis_angle, theta) +// } + +// // Noise +// (Depolarize, &QubitAndFloat(addr, p)) => $self.state.depolarize1(addr, p), +// (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { +// $self.state.depolarize2(addr0, addr1, p) +// } +// (PauliError, QubitAndFloatArr3(addr0, ps)) => { +// $self.state.pauli_error(*addr0, *ps) +// } +// (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { +// $self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) +// } + +// // Truncate: pruning per the configured strategy. +// (Truncate, None) => $self.state.truncate(), + +// // Batched arms: simple for-loop dispatch (no dedicated batch +// // methods on PauliSum, unlike GeneralizedTableau). +// (X, QubitBatch(addrs)) => $self.state.x_many(addrs), +// (Y, QubitBatch(addrs)) => $self.state.y_many(addrs), +// (Z, QubitBatch(addrs)) => $self.state.z_many(addrs), +// (H, QubitBatch(addrs)) => $self.state.h_many(addrs), +// (S, QubitBatch(addrs)) => $self.state.s_many(addrs), +// (SAdj, QubitBatch(addrs)) => $self.state.s_dag_many(addrs), +// (SqrtX, QubitBatch(addrs)) => $self.state.sqrt_x_many(addrs), +// (SqrtY, QubitBatch(addrs)) => $self.state.sqrt_y_many(addrs), +// (SqrtXAdj, QubitBatch(addrs)) => $self.state.sqrt_x_dag_many(addrs), +// (SqrtYAdj, QubitBatch(addrs)) => $self.state.sqrt_y_dag_many(addrs), +// (RX, QubitBatchAndFloat(addrs, angle)) => { +// $self.state.rx_many(addrs, *angle) +// } +// (RY, QubitBatchAndFloat(addrs, angle)) => { +// $self.state.ry_many(addrs, *angle) +// } +// (RZ, QubitBatchAndFloat(addrs, angle)) => { +// $self.state.rz_many(addrs, *angle) +// } +// (Depolarize, QubitBatchAndFloat(addrs, p)) => { +// batch_for!($self.state, depolarize1, addrs, *p) +// } +// (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { +// batch_for!($self.state, pauli_error, addrs, *ps) +// } +// (CNOT, TwoQubitBatch(pairs)) => $self.state.cnot_many(pairs), +// (CZ, TwoQubitBatch(pairs)) => $self.state.cz_many(pairs), +// (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { +// $self.state.rxx_many(pairs, *angle) +// } +// (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { +// $self.state.ryy_many(pairs, *angle) +// } +// (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { +// $self.state.rzz_many(pairs, *angle) +// } +// (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { +// batch_pairs_for!($self.state, depolarize2, pairs, *p) +// } +// (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { +// batch_pairs_for!($self.state, two_qubit_pauli_error, pairs, *ps) +// } + +// // Not supported on either backend (Decision 11 + Gate Support +// // Matrix). Loss / CorrelatedLoss handling differs by backend +// // and lives in the caller's impl block, not this macro. +// // Leakage is GeneralizedTableau-only (pinned |0⟩/|1⟩), not a +// // LossyPauliSum qutrit |L⟩ channel. +// (Measure | Reset | Leakage, _) => { +// return Err(eyre!("{} is not supported on the {} backend", $inst, $backend)); +// } + +// (T, Qubit(addr)) => $self.state.rz(*addr, std::f64::consts::PI / 8.0), +// (T, QubitBatch(addrs)) => $self.state.rz_many(addrs, std::f64::consts::PI / 8.0), +// (TAdj, Qubit(addr)) => $self.state.rz(*addr, -std::f64::consts::PI / 8.0), +// (TAdj, QubitBatch(addrs)) => $self.state.rz_many(addrs, -std::f64::consts::PI / 8.0), + +// // U3(θ, φ, λ) = RZ(φ)·RY(θ)·RZ(λ), matching the tableau / Python +// // `u3` (which applies rz(λ); ry(θ); rz(φ)). PauliSum is the +// // Heisenberg picture, so the sub-rotations run in reversed order +// // with the same angles: rz(φ); ry(θ); rz(λ). +// (U3, &QubitU3(addr, theta, phi, lam)) => { +// $self.state.rz(addr, phi); +// $self.state.ry(addr, theta); +// $self.state.rz(addr, lam); +// } + +// (U3, QubitBatchU3(addrs, theta, phi, lam)) => { +// $self.state.rz_many(addrs, *phi); +// $self.state.ry_many(addrs, *theta); +// $self.state.rz_many(addrs, *lam); +// } + +// // Trace: parse the resolved pattern string and compute the +// // trace. Per plan Decision 9, parsing happens on every +// // execution; no module-load caching. +// (Trace, PauliPatternStr(s)) => { +// let pat = PauliPattern::parse(s) +// .map_err(|e| eyre!("invalid Pauli pattern `{}`: {:?}", s, e))?; +// let value = $self.state.trace(&pat); +// return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { +// value, +// }))); +// } + +// // Fallback (mismatched shapes, etc.) +// (inst, msg) => { +// return Err(eyre!( +// "Invalid circuit instruction arguments {:?} for instruction {:?} on the {} backend", +// msg, +// inst, +// $backend +// )); +// } +// }; +// Ok(Effects::None) +// }}; +// } + +// /// PauliSum-backed executor (Heisenberg picture). Holds a `PauliSum` and +// /// answers the same `CircuitInstruction` vocabulary as `CircuitExecutor`, +// /// but without measurement / reset / loss support. +// pub struct PauliSumExecutor> { +// pub state: PauliSum, +// /// Snapshot of the seeded observable, restored by `reset`. +// initial: PauliSum, +// } + +// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +// impl PauliSumExecutor +// where +// T: Config, +// for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, +// { +// fn execute( +// &mut self, +// inst: CircuitInstruction, +// msg: CircuitMessage, +// ) -> Result> { +// self.execute_instruction(&inst, &msg) +// } + +// fn execute_instruction( +// &mut self, +// inst: &CircuitInstruction, +// msg: &CircuitMessage, +// ) -> Result> { +// use CircuitInstruction::*; + +// if matches!(inst, Loss | CorrelatedLoss) { +// return Err(eyre!( +// "{inst} is not supported on the PauliSum backend; use the LossyPauliSum backend instead" +// )); +// } + +// dispatch_common_paulisum!(self, inst, msg, "PauliSum") +// } +// } + +// impl vihaco::Reset for PauliSumExecutor +// where +// T: Config, +// PauliSum: Clone, +// { +// fn reset(&mut self) { +// self.state = self.initial.clone(); +// } +// } + +// /// LossyPauliSum-backed executor. Same dispatch as `PauliSumExecutor` plus +// /// `Loss` / `CorrelatedLoss` channels. The concrete `T` used by the +// /// enclosing `Circuit::LossyPauliSum` variant is a `Config` whose +// /// `PauliWordType` is `LossyPauliWord` (see `LossyPauliSumConfig`). +// pub struct LossyPauliSumExecutor> { +// pub state: PauliSum, +// /// Snapshot of the seeded observable, restored by `reset`. +// initial: PauliSum, +// } + +// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +// impl LossyPauliSumExecutor +// where +// T: Config, +// for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, +// { +// fn execute( +// &mut self, +// inst: CircuitInstruction, +// msg: CircuitMessage, +// ) -> Result> { +// self.execute_instruction(&inst, &msg) +// } + +// fn execute_instruction( +// &mut self, +// inst: &CircuitInstruction, +// msg: &CircuitMessage, +// ) -> Result> { +// use CircuitInstruction::*; +// use CircuitMessage::*; + +// // Loss / CorrelatedLoss are the only instructions that differ from +// // PauliSum; handle them here then delegate everything else to the +// // shared dispatch. +// match (inst, msg) { +// (Loss, &QubitAndFloat(addr, p)) => { +// self.state.loss_channel(addr, p); +// return Ok(Effects::None); +// } +// (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { +// self.state.correlated_loss_channel(*addr0, *addr1, *ps); +// return Ok(Effects::None); +// } +// (Loss, QubitBatchAndFloat(addrs, p)) => { +// batch_for!(self.state, loss_channel, addrs, *p); +// return Ok(Effects::None); +// } +// (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { +// batch_pairs_for!(self.state, correlated_loss_channel, pairs, *ps); +// return Ok(Effects::None); +// } +// _ => {} +// } + +// dispatch_common_paulisum!(self, inst, msg, "LossyPauliSum") +// } +// } + +// impl vihaco::Reset for LossyPauliSumExecutor +// where +// T: Config, +// PauliSum: Clone, +// { +// fn reset(&mut self) { +// self.state = self.initial.clone(); +// } +// } + +// /// Tableau-backed inner enum (Schrödinger picture). Carries the six +// /// size-bucketed `CircuitExecutor` variants; bucket is picked from `n_qubits`. +// pub enum TableauCircuit { +// Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>), +// Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>), +// Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>), +// Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>), +// Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>), +// Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>), +// } + +// impl TableauCircuit { +// pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Result { +// if n_qubits <= 64 { +// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); +// Ok(Self::Bits64(CircuitExecutor { tab })) +// } else if n_qubits <= 128 { +// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); +// Ok(Self::Bits128(CircuitExecutor { tab })) +// } else if n_qubits <= 256 { +// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); +// Ok(Self::Bits256(CircuitExecutor { tab })) +// } else if n_qubits <= 512 { +// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); +// Ok(Self::Bits512(CircuitExecutor { tab })) +// } else if n_qubits <= 1024 { +// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); +// Ok(Self::Bits1024(CircuitExecutor { tab })) +// } else if n_qubits <= MAX_QUBITS { +// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); +// Ok(Self::Bits2048(CircuitExecutor { tab })) +// } else { +// Err(eyre!( +// "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" +// )) +// } +// } + +// /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a +// /// shot is reproducible. +// pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Result { +// macro_rules! seeded { +// ($variant:ident) => {{ +// let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); +// Ok(Self::$variant(CircuitExecutor { tab })) +// }}; +// } +// if n_qubits <= 64 { +// seeded!(Bits64) +// } else if n_qubits <= 128 { +// seeded!(Bits128) +// } else if n_qubits <= 256 { +// seeded!(Bits256) +// } else if n_qubits <= 512 { +// seeded!(Bits512) +// } else if n_qubits <= 1024 { +// seeded!(Bits1024) +// } else if n_qubits <= MAX_QUBITS { +// seeded!(Bits2048) +// } else { +// Err(eyre!( +// "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" +// )) +// } +// } + +// fn execute_instruction( +// &mut self, +// inst: &CircuitInstruction, +// msg: &CircuitMessage, +// ) -> Result> { +// match self { +// Self::Bits64(ex) => ex.execute_instruction(inst, msg), +// Self::Bits128(ex) => ex.execute_instruction(inst, msg), +// Self::Bits256(ex) => ex.execute_instruction(inst, msg), +// Self::Bits512(ex) => ex.execute_instruction(inst, msg), +// Self::Bits1024(ex) => ex.execute_instruction(inst, msg), +// Self::Bits2048(ex) => ex.execute_instruction(inst, msg), +// } +// } + +// pub fn state_string(&self) -> String { +// match self { +// Self::Bits64(ex) => ex.tab.to_string(), +// Self::Bits128(ex) => ex.tab.to_string(), +// Self::Bits256(ex) => ex.tab.to_string(), +// Self::Bits512(ex) => ex.tab.to_string(), +// Self::Bits1024(ex) => ex.tab.to_string(), +// Self::Bits2048(ex) => ex.tab.to_string(), +// } +// } +// } + +// impl vihaco::Reset for TableauCircuit { +// fn reset(&mut self) { +// match self { +// Self::Bits64(ex) => ex.reset(), +// Self::Bits128(ex) => ex.reset(), +// Self::Bits256(ex) => ex.reset(), +// Self::Bits512(ex) => ex.reset(), +// Self::Bits1024(ex) => ex.reset(), +// Self::Bits2048(ex) => ex.reset(), +// }; +// } +// } + +// /// PauliSum-backed inner enum (Heisenberg picture). Per Decision 7 of the plan, +// /// the size buckets carry `[u8; N]`-storage `ByteFxHashF64` configs (N = 8, 16, +// /// …, 256) rather than the tableau's `[u64; N]` configs; bucket labels match +// /// the semantic qubit count (`Bits64` = 64 qubits) so the outer enum's dispatch +// /// is uniform across backends. +// pub enum PauliSumCircuit { +// Bits64(PauliSumExecutor>), +// Bits128(PauliSumExecutor>), +// Bits256(PauliSumExecutor>), +// Bits512(PauliSumExecutor>), +// Bits1024(PauliSumExecutor>), +// Bits2048(PauliSumExecutor>), +// } + +// impl PauliSumCircuit { +// /// Build a PauliSum-backed circuit, seeding the state with every term: +// /// `for (word, coef) in terms { state += (word, coef); }`. Words must +// /// already be validated against `info.n_qubits` by the caller. +// pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { +// macro_rules! build { +// ($variant:ident, $N:literal) => {{ +// let mut state = PauliSum::>::builder() +// .n_qubits(info.n_qubits) +// .strategy(paulisum_strategy(info)) +// .build(); +// for (word, coef) in terms { +// state += (word.as_str(), *coef); +// } +// let initial = state.clone(); +// Ok(Self::$variant(PauliSumExecutor { state, initial })) +// }}; +// } +// if info.n_qubits <= 64 { +// build!(Bits64, 8) +// } else if info.n_qubits <= 128 { +// build!(Bits128, 16) +// } else if info.n_qubits <= 256 { +// build!(Bits256, 32) +// } else if info.n_qubits <= 512 { +// build!(Bits512, 64) +// } else if info.n_qubits <= 1024 { +// build!(Bits1024, 128) +// } else if info.n_qubits <= MAX_QUBITS { +// build!(Bits2048, 256) +// } else { +// Err(eyre!( +// "cannot simulate {} qubits: maximum is {MAX_QUBITS}", +// info.n_qubits +// )) +// } +// } + +// fn execute_instruction( +// &mut self, +// inst: &CircuitInstruction, +// msg: &CircuitMessage, +// ) -> Result> { +// match self { +// Self::Bits64(ex) => ex.execute_instruction(inst, msg), +// Self::Bits128(ex) => ex.execute_instruction(inst, msg), +// Self::Bits256(ex) => ex.execute_instruction(inst, msg), +// Self::Bits512(ex) => ex.execute_instruction(inst, msg), +// Self::Bits1024(ex) => ex.execute_instruction(inst, msg), +// Self::Bits2048(ex) => ex.execute_instruction(inst, msg), +// } +// } + +// pub fn state_string(&self) -> String { +// match self { +// Self::Bits64(ex) => ex.state.to_string(), +// Self::Bits128(ex) => ex.state.to_string(), +// Self::Bits256(ex) => ex.state.to_string(), +// Self::Bits512(ex) => ex.state.to_string(), +// Self::Bits1024(ex) => ex.state.to_string(), +// Self::Bits2048(ex) => ex.state.to_string(), +// } +// } +// } + +// impl vihaco::Reset for PauliSumCircuit { +// fn reset(&mut self) { +// match self { +// Self::Bits64(ex) => ex.reset(), +// Self::Bits128(ex) => ex.reset(), +// Self::Bits256(ex) => ex.reset(), +// Self::Bits512(ex) => ex.reset(), +// Self::Bits1024(ex) => ex.reset(), +// Self::Bits2048(ex) => ex.reset(), +// }; +// } +// } + +// /// LossyPauliSum-backed inner enum. Identical shape to [`PauliSumCircuit`] +// /// but with `LossyPauliWord`-keyed configs so loss-channel methods dispatch. +// pub enum LossyPauliSumCircuit { +// Bits64(LossyPauliSumExecutor>), +// Bits128(LossyPauliSumExecutor>), +// Bits256(LossyPauliSumExecutor>), +// Bits512(LossyPauliSumExecutor>), +// Bits1024(LossyPauliSumExecutor>), +// Bits2048(LossyPauliSumExecutor>), +// } + +// impl LossyPauliSumCircuit { +// /// Build a LossyPauliSum-backed circuit, seeding every term via +// /// `state += (word, coef)`. Words must already be validated against +// /// `info.n_qubits` by the caller. +// pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { +// macro_rules! build { +// ($variant:ident, $N:literal) => {{ +// let mut state = PauliSum::>::builder() +// .n_qubits(info.n_qubits) +// .strategy(paulisum_strategy(info)) +// .build(); +// for (word, coef) in terms { +// state += (word.as_str(), *coef); +// } +// let initial = state.clone(); +// Ok(Self::$variant(LossyPauliSumExecutor { state, initial })) +// }}; +// } +// if info.n_qubits <= 64 { +// build!(Bits64, 8) +// } else if info.n_qubits <= 128 { +// build!(Bits128, 16) +// } else if info.n_qubits <= 256 { +// build!(Bits256, 32) +// } else if info.n_qubits <= 512 { +// build!(Bits512, 64) +// } else if info.n_qubits <= 1024 { +// build!(Bits1024, 128) +// } else if info.n_qubits <= MAX_QUBITS { +// build!(Bits2048, 256) +// } else { +// Err(eyre!( +// "cannot simulate {} qubits: maximum is {MAX_QUBITS}", +// info.n_qubits +// )) +// } +// } + +// fn execute_instruction( +// &mut self, +// inst: &CircuitInstruction, +// msg: &CircuitMessage, +// ) -> Result> { +// match self { +// Self::Bits64(ex) => ex.execute_instruction(inst, msg), +// Self::Bits128(ex) => ex.execute_instruction(inst, msg), +// Self::Bits256(ex) => ex.execute_instruction(inst, msg), +// Self::Bits512(ex) => ex.execute_instruction(inst, msg), +// Self::Bits1024(ex) => ex.execute_instruction(inst, msg), +// Self::Bits2048(ex) => ex.execute_instruction(inst, msg), +// } +// } + +// pub fn state_string(&self) -> String { +// match self { +// Self::Bits64(ex) => ex.state.to_string(), +// Self::Bits128(ex) => ex.state.to_string(), +// Self::Bits256(ex) => ex.state.to_string(), +// Self::Bits512(ex) => ex.state.to_string(), +// Self::Bits1024(ex) => ex.state.to_string(), +// Self::Bits2048(ex) => ex.state.to_string(), +// } +// } +// } + +// impl vihaco::Reset for LossyPauliSumCircuit { +// fn reset(&mut self) { +// match self { +// Self::Bits64(ex) => ex.reset(), +// Self::Bits128(ex) => ex.reset(), +// Self::Bits256(ex) => ex.reset(), +// Self::Bits512(ex) => ex.reset(), +// Self::Bits1024(ex) => ex.reset(), +// Self::Bits2048(ex) => ex.reset(), +// }; +// } +// } + +// /// Outer `Circuit` enum: backend selector. Picks one of the three inner enums +// /// based on `info.backend` at construction time; from there, every per-step +// /// call routes outer → inner → executor. +// pub enum Circuit { +// Tableau(TableauCircuit), +// PauliSum(PauliSumCircuit), +// LossyPauliSum(LossyPauliSumCircuit), +// } + +// impl vihaco::HasInstructionSet for Circuit { +// type Runtime = vihaco_circuit_isa::CircuitInstruction; +// type Syntax = vihaco_circuit_isa::CircuitSurfaceInstruction; +// } + +// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +// impl Circuit { +// /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and +// /// `coefficient_threshold` from `info`; no observable required. +// pub fn tableau(info: &PPVMDeviceInfo) -> Result { +// Ok(Self::Tableau(TableauCircuit::new( +// info.n_qubits, +// info.coefficient_threshold, +// )?)) +// } + +// /// Same as [`Circuit::tableau`], but seed the tableau RNG deterministically +// /// so a shot is reproducible. +// pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { +// Ok(Self::Tableau(TableauCircuit::new_with_seed( +// info.n_qubits, +// info.coefficient_threshold, +// seed, +// )?)) +// } + +// /// Build a PauliSum-backed circuit, seeding the state with every term in +// /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the +// /// caller is responsible for having parsed/validated the words against +// /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). +// pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { +// Ok(Self::PauliSum(PauliSumCircuit::new(info, terms)?)) +// } + +// /// Build a LossyPauliSum-backed circuit. Same contract as +// /// [`Circuit::paulisum`]. +// pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { +// Ok(Self::LossyPauliSum(LossyPauliSumCircuit::new(info, terms)?)) +// } + +// fn execute( +// &mut self, +// inst: CircuitInstruction, +// msg: CircuitMessage, +// ) -> Result> { +// self.execute_instruction(&inst, &msg) +// } + +// fn execute_instruction( +// &mut self, +// inst: &CircuitInstruction, +// msg: &CircuitMessage, +// ) -> Result> { +// match self { +// Self::Tableau(c) => c.execute_instruction(inst, msg), +// Self::PauliSum(c) => c.execute_instruction(inst, msg), +// Self::LossyPauliSum(c) => c.execute_instruction(inst, msg), +// } +// } + +// /// Render the current state. Used by the REPL's `show` command. +// pub fn state_string(&self) -> String { +// match self { +// Self::Tableau(c) => c.state_string(), +// Self::PauliSum(c) => c.state_string(), +// Self::LossyPauliSum(c) => c.state_string(), +// } +// } +// } + +// #[observe(CircuitEffect, effect=CircuitOutcomeEffect)] +// impl Circuit { +// fn observe_circuit_effect( +// &mut self, +// effect: &CircuitEffect, +// ) -> Result> { +// self.execute_instruction(&effect.inst, &effect.msg) +// } +// } + +// impl vihaco::Reset for Circuit { +// fn reset(&mut self) { +// match self { +// Self::Tableau(c) => c.reset(), +// Self::PauliSum(c) => c.reset(), +// Self::LossyPauliSum(c) => c.reset(), +// }; +// } +// } + +// impl Default for Circuit { +// fn default() -> Self { +// // Default backend is Tableau with 0 qubits, which always fits the +// // smallest bucket, so construction here is infallible. +// Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") +// } +// } + +// #[cfg(test)] +// mod tests { +// use super::*; + +// fn info(n_qubits: usize) -> PPVMDeviceInfo { +// PPVMDeviceInfo { +// n_qubits, +// ..Default::default() +// } +// } + +// /// Dispatch a single-qubit `Measure` and read the outcome out of the +// /// returned effect. +// fn measure(circuit: &mut Circuit, addr: usize) -> MeasurementOutcome { +// let effects = circuit +// .execute_instruction(&CircuitInstruction::Measure, &CircuitMessage::Qubit(addr)) +// .unwrap(); +// match effects.into_iter().next() { +// Some(CircuitOutcomeEffect::Measurement(m)) => m.measurement_results[0], +// other => panic!("expected a measurement effect, got {other:?}"), +// } +// } + +// /// Smoke test: a single-qubit gate dispatches to the tableau backend and +// /// flips the qubit. +// #[test] +// fn tableau_backend_x_flips_qubit() { +// let mut circuit = Circuit::tableau(&info(1)).unwrap(); +// circuit +// .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) +// .unwrap(); +// assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); +// } + +// /// Smoke test: a two-qubit gate dispatches correctly — `X(0); CNOT(0, 1)` +// /// leaves both qubits in |1⟩. +// #[test] +// fn tableau_backend_cnot_propagates_flip() { +// let mut circuit = Circuit::tableau(&info(2)).unwrap(); +// circuit +// .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) +// .unwrap(); +// circuit +// .execute_instruction(&CircuitInstruction::CNOT, &CircuitMessage::TwoQubit(0, 1)) +// .unwrap(); +// assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); +// assert_eq!(measure(&mut circuit, 1), MeasurementOutcome::One); +// } + +// // ─── Construction rejects more qubits than the backend ceiling ──────── +// // +// // The widest executor bucket is 2048 qubits (U2048 / `[u8; 256]`); beyond +// // that there is no backing width, so the constructors return an error +// // rather than panicking — a panic would tear down the TUI that drives this. + +// #[test] +// fn tableau_rejects_more_than_2048_qubits() { +// assert!(Circuit::tableau(&info(MAX_QUBITS + 1)).is_err()); +// } + +// #[test] +// fn tableau_with_seed_rejects_more_than_2048_qubits() { +// assert!(Circuit::tableau_with_seed(&info(MAX_QUBITS + 1), 0).is_err()); +// } + +// #[test] +// fn paulisum_rejects_more_than_2048_qubits() { +// assert!(Circuit::paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); +// } + +// #[test] +// fn lossy_paulisum_rejects_more_than_2048_qubits() { +// assert!(Circuit::lossy_paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); +// } + +// #[test] +// fn constructs_at_the_2048_qubit_boundary() { +// // 2048 is the last valid bucket; it must still succeed. +// assert!(Circuit::tableau(&info(MAX_QUBITS)).is_ok()); +// } +// } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 2d8f4e5e7..4e9e1a6a7 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,119 +1,118 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -pub mod bytecode; +// pub mod bytecode; pub mod component; -pub mod composite; +// pub mod composite; pub mod device_info; pub mod measurements; pub mod observable; -pub mod shots; -mod syntax; +// pub mod shots; +// mod syntax; /// Re-exported so consumers (e.g. the CLI REPL) can name gates for /// [`composite::PPVM::apply_circuit_instruction`] without depending on the ISA /// crate directly. -pub use vihaco_circuit_isa::CircuitInstruction; - -use vihaco::syntax::{ParsedModule, Resolve}; +// pub use vihaco_circuit_isa::CircuitInstruction; +use vihaco::syntax::ParsedModule; use vihaco::{Type, Value, module::LocalModule}; -use crate::composite::ppvm_module as ppvm; -use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; -use crate::syntax::{PPVMHeader, PPVMResolver, parse_functions, parse_headers}; - -/// A fully resolved PPVM module, ready to load into a [`PPVM`]. -pub type PPVMModule = LocalModule; - -/// Read a file and produce a loadable module, auto-detecting the format: a -/// leading PPVM magic is parsed as `.ssb` bytecode, otherwise as `.sst` source. -/// Mirrors [`PPVM::load_file`] but returns the module so it can be compiled once -/// and run for many shots. -pub fn load_module_file(path: &str) -> eyre::Result { - let bytes = std::fs::read(path)?; - if bytecode::is_bytecode(&bytes) { - bytecode::module_from_bytes(&bytes) - } else { - compile_program(std::str::from_utf8(&bytes)?) - } -} - -pub fn run_file(path: &str) -> eyre::Result { - let mut machine = PPVM::default(); - machine.run_file(path)?; - Ok(machine) -} - -pub fn run_program(program: &str) -> eyre::Result { - let mut machine = PPVM::default(); - machine.run_program(program)?; - Ok(machine) -} - -/// Parse `.sst` source into the unresolved AST. -pub fn parse_program( - source: &str, -) -> eyre::Result>> -{ - let (header, body) = parse_headers(source)?; - Ok(ParsedModule { - header, - functions: parse_functions(&body)?, - }) -} - -pub fn compile_program(source: &str) -> eyre::Result { - PPVMResolver::new().resolve_module(parse_program(source)?) -} - -/// Dump `.sst` source to a `.ssb` bytecode file. -pub fn dump_program(program: &str, output_path: &str) -> eyre::Result<()> { - let bytes = bytecode::compile_to_bytes(program)?; - std::fs::write(output_path, bytes)?; - Ok(()) -} - -/// Read a `.sst` file and dump it to a `.ssb` bytecode file. -pub fn dump_file(input_path: &str, output_path: &str) -> eyre::Result<()> { - let program = std::fs::read_to_string(input_path)?; - dump_program(&program, output_path) -} - -pub mod prelude { - pub use crate::component::Circuit; - pub use crate::composite::PPVM; - pub use crate::syntax::{PPVMHeader, PPVMResolver}; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn dump_program_writes_loadable_bytecode() { - let src = "device circuit.n_qubits 1;\n\ - fn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; - let path = std::env::temp_dir().join("ppvm_dump_program_test.ssb"); - dump_program(src, path.to_str().unwrap()).unwrap(); - - let mut machine = PPVM::default(); - machine.load_bytecode_file(path.to_str().unwrap()).unwrap(); - machine.run().unwrap(); - - assert_eq!(machine.measurement_record().len(), 1); - let _ = std::fs::remove_file(&path); - } - - #[test] - fn dump_file_reads_sst_and_writes_bytecode() { - let out = std::env::temp_dir().join("ppvm_dump_file_test.ssb"); - dump_file("tests/function_call.sst", out.to_str().unwrap()).unwrap(); - - let mut machine = PPVM::default(); - machine.load_bytecode_file(out.to_str().unwrap()).unwrap(); - machine.run().unwrap(); - - assert_eq!(machine.measurement_record().len(), 1); - let _ = std::fs::remove_file(&out); - } -} +// use crate::composite::ppvm_module as ppvm; +// use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; +// use crate::syntax::{PPVMHeader, PPVMResolver, parse_functions, parse_headers}; + +// /// A fully resolved PPVM module, ready to load into a [`PPVM`]. +// pub type PPVMModule = LocalModule; + +// /// Read a file and produce a loadable module, auto-detecting the format: a +// /// leading PPVM magic is parsed as `.ssb` bytecode, otherwise as `.sst` source. +// /// Mirrors [`PPVM::load_file`] but returns the module so it can be compiled once +// /// and run for many shots. +// pub fn load_module_file(path: &str) -> eyre::Result { +// let bytes = std::fs::read(path)?; +// if bytecode::is_bytecode(&bytes) { +// bytecode::module_from_bytes(&bytes) +// } else { +// compile_program(std::str::from_utf8(&bytes)?) +// } +// } + +// pub fn run_file(path: &str) -> eyre::Result { +// let mut machine = PPVM::default(); +// machine.run_file(path)?; +// Ok(machine) +// } + +// pub fn run_program(program: &str) -> eyre::Result { +// let mut machine = PPVM::default(); +// machine.run_program(program)?; +// Ok(machine) +// } + +// // /// Parse `.sst` source into the unresolved AST. +// // pub fn parse_program( +// // source: &str, +// // ) -> eyre::Result>> +// // { +// // let (header, body) = parse_headers(source)?; +// // Ok(ParsedModule { +// // header, +// // functions: parse_functions(&body)?, +// // }) +// // } + +// pub fn compile_program(source: &str) -> eyre::Result { +// PPVMResolver::new().resolve_module(parse_program(source)?) +// } + +// /// Dump `.sst` source to a `.ssb` bytecode file. +// pub fn dump_program(program: &str, output_path: &str) -> eyre::Result<()> { +// let bytes = bytecode::compile_to_bytes(program)?; +// std::fs::write(output_path, bytes)?; +// Ok(()) +// } + +// /// Read a `.sst` file and dump it to a `.ssb` bytecode file. +// pub fn dump_file(input_path: &str, output_path: &str) -> eyre::Result<()> { +// let program = std::fs::read_to_string(input_path)?; +// dump_program(&program, output_path) +// } + +// pub mod prelude { +// // pub use crate::component::Circuit; +// pub use crate::composite::PPVM; +// pub use crate::syntax::{PPVMHeader, PPVMResolver}; +// } + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn dump_program_writes_loadable_bytecode() { +// let src = "device circuit.n_qubits 1;\n\ +// fn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; +// let path = std::env::temp_dir().join("ppvm_dump_program_test.ssb"); +// dump_program(src, path.to_str().unwrap()).unwrap(); + +// let mut machine = PPVM::default(); +// machine.load_bytecode_file(path.to_str().unwrap()).unwrap(); +// machine.run().unwrap(); + +// assert_eq!(machine.measurement_record().len(), 1); +// let _ = std::fs::remove_file(&path); +// } + +// #[test] +// fn dump_file_reads_sst_and_writes_bytecode() { +// let out = std::env::temp_dir().join("ppvm_dump_file_test.ssb"); +// dump_file("tests/function_call.sst", out.to_str().unwrap()).unwrap(); + +// let mut machine = PPVM::default(); +// machine.load_bytecode_file(out.to_str().unwrap()).unwrap(); +// machine.run().unwrap(); + +// assert_eq!(machine.measurement_record().len(), 1); +// let _ = std::fs::remove_file(&out); +// } +// } From 5ed9ed901fde038958516f708384cc85705fea06 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 3 Nov 2026 15:20:58 +0100 Subject: [PATCH 04/36] Get rid of the enum --- crates/ppvm-vihaco/src/component.rs | 875 +--------------- crates/ppvm-vihaco/src/lib.rs | 2 +- crates/ppvm-vihaco/tests/sst_fixtures.rs | 1168 +++++++++++----------- 3 files changed, 587 insertions(+), 1458 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 394b552ea..cee35a059 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -192,6 +192,8 @@ single_qubit_rotation_execute!(Depolarize, depolarize1); single_qubit_rotation_execute!(Loss, loss_channel); two_qubit_rotation_execute!(Depolarize2, depolarize2); +// TODO: batched gates; need custom messages + impl Execute for CircuitExecutor where T: Config, @@ -348,199 +350,6 @@ where } } -// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -// impl CircuitExecutor -// where -// T: Config, -// <::Storage as BitView>::Store: PrimInt, -// I: TableauIndex + Send + Sync + std::fmt::Debug, -// C: SparseVector + std::fmt::Debug, -// { -// fn execute( -// &mut self, -// inst: CircuitInstruction, -// msg: CircuitMessage, -// ) -> Result> { -// self.execute_instruction(&inst, &msg) -// } - -// fn execute_instruction( -// &mut self, -// inst: &CircuitInstruction, -// msg: &CircuitMessage, -// ) -> Result> { -// use CircuitInstruction::*; -// use CircuitMessage::*; - -// match (inst, msg) { -// // Single-qubit Clifford -// (X, &Qubit(addr)) => self.tab.x(addr), -// (Y, &Qubit(addr)) => self.tab.y(addr), -// (Z, &Qubit(addr)) => self.tab.z(addr), -// (H, &Qubit(addr)) => self.tab.h(addr), -// (S, &Qubit(addr)) => self.tab.s(addr), -// (SAdj, &Qubit(addr)) => self.tab.s_dag(addr), -// (SqrtX, &Qubit(addr)) => self.tab.sqrt_x(addr), -// (SqrtY, &Qubit(addr)) => self.tab.sqrt_y(addr), -// (SqrtXAdj, &Qubit(addr)) => self.tab.sqrt_x_dag(addr), -// (SqrtYAdj, &Qubit(addr)) => self.tab.sqrt_y_dag(addr), - -// // Controlled gates -// (CNOT, &TwoQubit(addr0, addr1)) => self.tab.cnot(addr0, addr1), -// (CZ, &TwoQubit(addr0, addr1)) => self.tab.cz(addr0, addr1), - -// // T gate -// (T, &Qubit(addr)) => self.tab.t(addr), -// (TAdj, &Qubit(addr)) => self.tab.t_dag(addr), - -// // Single-qubit rotations -// (RX, &QubitAndFloat(addr, angle)) => self.tab.rx(addr, angle), -// (RY, &QubitAndFloat(addr, angle)) => self.tab.ry(addr, angle), -// (RZ, &QubitAndFloat(addr, angle)) => self.tab.rz(addr, angle), - -// // Two-qubit rotations -// (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rxx(addr0, addr1, angle), -// (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.ryy(addr0, addr1, angle), -// (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rzz(addr0, addr1, angle), - -// // U3 -// (U3, &QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), - -// // RXY: rotation about an axis in the x/y plane -// (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => self.tab.r(addr, axis_angle, theta), - -// // Measure & Reset -// (Measure, &Qubit(addr)) => { -// let outcome: MeasurementOutcome = self.tab.measure(addr).into(); -// return Ok(Effects::one(CircuitOutcomeEffect::Measurement( -// MeasurementEffect { -// measurement_results: smallvec::smallvec![outcome], -// }, -// ))); -// } -// (Reset, &Qubit(addr)) => self.tab.reset(addr), - -// // Noise -// (Depolarize, &QubitAndFloat(addr, p)) => self.tab.depolarize1(addr, p), -// (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { -// self.tab.depolarize2(addr0, addr1, p) -// } -// (PauliError, QubitAndFloatArr3(addr0, ps)) => self.tab.pauli_error(*addr0, *ps), -// (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { -// self.tab.two_qubit_pauli_error(*addr0, *addr1, *ps) -// } - -// // Loss -// (Loss, &QubitAndFloat(addr, p)) => self.tab.loss_channel(addr, p), -// (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { -// self.tab.correlated_loss_channel(*addr0, *addr1, *ps) -// } -// (Leakage, &QubitAndTwoFloats(addr, p0, p1)) => self.tab.leakage_channel(addr, p0, p1), - -// /* BATCH OPERATIONS START HERE */ -// // Batch: dedicated batch methods -// (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_many(addrs), -// (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_many(addrs), -// (SqrtXAdj, QubitBatch(addrs)) => self.tab.sqrt_x_dag_many(addrs), -// (SqrtYAdj, QubitBatch(addrs)) => self.tab.sqrt_y_dag_many(addrs), -// (H, QubitBatch(addrs)) => self.tab.h_many(addrs), -// (CZ, TwoQubitBatch(pairs)) => self.tab.cz_many(pairs), - -// // TODO: replace things below by actual batched methods once they are available -// // Batch: single-qubit for loops -// (X, QubitBatch(addrs)) => self.tab.x_many(addrs), -// (Y, QubitBatch(addrs)) => self.tab.y_many(addrs), -// (Z, QubitBatch(addrs)) => self.tab.z_many(addrs), -// (S, QubitBatch(addrs)) => self.tab.s_many(addrs), -// (SAdj, QubitBatch(addrs)) => self.tab.s_dag_many(addrs), -// (T, QubitBatch(addrs)) => self.tab.t_many(addrs), -// (TAdj, QubitBatch(addrs)) => self.tab.t_dag_many(addrs), -// (Reset, QubitBatch(addrs)) => self.tab.reset_many(addrs), -// (RX, QubitBatchAndFloat(addrs, angle)) => self.tab.rx_many(addrs, *angle), -// (RY, QubitBatchAndFloat(addrs, angle)) => self.tab.ry_many(addrs, *angle), -// (RZ, QubitBatchAndFloat(addrs, angle)) => self.tab.rz_many(addrs, *angle), -// (Depolarize, QubitBatchAndFloat(addrs, p)) => { -// batch_for!(self.tab, depolarize1, addrs, *p) -// } -// (Loss, QubitBatchAndFloat(addrs, p)) => batch_for!(self.tab, loss_channel, addrs, *p), -// (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { -// batch_for!(self.tab, pauli_error, addrs, *ps) -// } -// (U3, QubitBatchU3(addrs, theta, phi, lam)) => { -// batch_for!(self.tab, u3, addrs, *theta, *phi, *lam) -// } - -// // Batch: two-qubit for loops -// (CNOT, TwoQubitBatch(pairs)) => { -// self.tab.cnot_many(pairs); -// } -// (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { -// self.tab.rxx_many(pairs, *angle); -// } -// (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { -// self.tab.ryy_many(pairs, *angle); -// } -// (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { -// self.tab.rzz_many(pairs, *angle); -// } -// (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { -// for &(a, b) in pairs { -// self.tab.depolarize2(a, b, *p); -// } -// } -// (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { -// for &(a, b) in pairs { -// self.tab.two_qubit_pauli_error(a, b, *ps); -// } -// } -// (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { -// for &(a, b) in pairs { -// self.tab.correlated_loss_channel(a, b, *ps); -// } -// } - -// // Batch: measure (emits per qubit) -// (Measure, QubitBatch(addrs)) => { -// let outcomes = self.tab.measure_many(addrs); -// return Ok(Effects::one(CircuitOutcomeEffect::Measurement( -// MeasurementEffect { -// measurement_results: outcomes.iter().map(|&o| o.into()).collect(), -// }, -// ))); -// } - -// // Truncate is a silent no-op on the Tableau backend — the tableau's -// // gate methods already prune via the configured coefficient -// // threshold, so there's nothing extra to do here. -// (Truncate, None) => {} - -// // Trace: parse the resolved pattern and compute Σ_{P matches} ⟨ψ|P|ψ⟩ -// // on the tableau state. Asymmetric with the PauliSum semantics by -// // design (Decision 9): on the tableau this is a sum of expectations, -// // not a coefficient filter. -// (Trace, PauliPatternStr(s)) => { -// let pat = PauliPattern::parse(s) -// .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; -// let value = self.tab.trace(&pat); -// return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { -// value, -// }))); -// } - -// // Fallback -// (inst, msg) => { -// return Err(eyre!( -// "Invalid circuit instruction arguments {:?} for instruction {:?}", -// msg, -// inst -// )); -// } -// }; - -// Ok(Effects::None) -// } -// } - impl vihaco::Reset for CircuitExecutor where T: Config, @@ -553,686 +362,6 @@ where } } -// /// Shared dispatch body for `PauliSumExecutor` and `LossyPauliSumExecutor`. -// /// Every non-loss `CircuitInstruction` lands here. `LossyPauliSumExecutor` -// /// matches `Loss` / `CorrelatedLoss` (single + batched) before invoking this -// /// macro and never reaches the loss-rejection arm below. -// /// -// /// `$self` is passed as an `ident` (typically `self`) so the macro's -// /// expansion shares hygiene with the surrounding method's `self` parameter. -// /// `$inst` / `$msg` are passed the same way; `$backend` is the human-readable -// /// backend name baked into error messages. -// macro_rules! dispatch_common_paulisum { -// ($self:ident, $inst:ident, $msg:ident, $backend:literal) => {{ -// use CircuitInstruction::*; -// use CircuitMessage::*; -// match ($inst, $msg) { -// // Single-qubit Clifford -// (X, &Qubit(addr)) => $self.state.x(addr), -// (Y, &Qubit(addr)) => $self.state.y(addr), -// (Z, &Qubit(addr)) => $self.state.z(addr), -// (H, &Qubit(addr)) => $self.state.h(addr), -// (S, &Qubit(addr)) => $self.state.s(addr), -// (SAdj, &Qubit(addr)) => $self.state.s_dag(addr), -// (SqrtX, &Qubit(addr)) => $self.state.sqrt_x(addr), -// (SqrtY, &Qubit(addr)) => $self.state.sqrt_y(addr), -// (SqrtXAdj, &Qubit(addr)) => $self.state.sqrt_x_dag(addr), -// (SqrtYAdj, &Qubit(addr)) => $self.state.sqrt_y_dag(addr), - -// // Controlled gates -// (CNOT, &TwoQubit(addr0, addr1)) => $self.state.cnot(addr0, addr1), -// (CZ, &TwoQubit(addr0, addr1)) => $self.state.cz(addr0, addr1), - -// // Single-qubit rotations -// (RX, &QubitAndFloat(addr, angle)) => $self.state.rx(addr, angle), -// (RY, &QubitAndFloat(addr, angle)) => $self.state.ry(addr, angle), -// (RZ, &QubitAndFloat(addr, angle)) => $self.state.rz(addr, angle), - -// // Two-qubit rotations -// (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => { -// $self.state.rxx(addr0, addr1, angle) -// } -// (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => { -// $self.state.ryy(addr0, addr1, angle) -// } -// (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => { -// $self.state.rzz(addr0, addr1, angle) -// } - -// // RXY: rotation about an axis in the x/y plane -// (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { -// $self.state.r(addr, axis_angle, theta) -// } - -// // Noise -// (Depolarize, &QubitAndFloat(addr, p)) => $self.state.depolarize1(addr, p), -// (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { -// $self.state.depolarize2(addr0, addr1, p) -// } -// (PauliError, QubitAndFloatArr3(addr0, ps)) => { -// $self.state.pauli_error(*addr0, *ps) -// } -// (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { -// $self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) -// } - -// // Truncate: pruning per the configured strategy. -// (Truncate, None) => $self.state.truncate(), - -// // Batched arms: simple for-loop dispatch (no dedicated batch -// // methods on PauliSum, unlike GeneralizedTableau). -// (X, QubitBatch(addrs)) => $self.state.x_many(addrs), -// (Y, QubitBatch(addrs)) => $self.state.y_many(addrs), -// (Z, QubitBatch(addrs)) => $self.state.z_many(addrs), -// (H, QubitBatch(addrs)) => $self.state.h_many(addrs), -// (S, QubitBatch(addrs)) => $self.state.s_many(addrs), -// (SAdj, QubitBatch(addrs)) => $self.state.s_dag_many(addrs), -// (SqrtX, QubitBatch(addrs)) => $self.state.sqrt_x_many(addrs), -// (SqrtY, QubitBatch(addrs)) => $self.state.sqrt_y_many(addrs), -// (SqrtXAdj, QubitBatch(addrs)) => $self.state.sqrt_x_dag_many(addrs), -// (SqrtYAdj, QubitBatch(addrs)) => $self.state.sqrt_y_dag_many(addrs), -// (RX, QubitBatchAndFloat(addrs, angle)) => { -// $self.state.rx_many(addrs, *angle) -// } -// (RY, QubitBatchAndFloat(addrs, angle)) => { -// $self.state.ry_many(addrs, *angle) -// } -// (RZ, QubitBatchAndFloat(addrs, angle)) => { -// $self.state.rz_many(addrs, *angle) -// } -// (Depolarize, QubitBatchAndFloat(addrs, p)) => { -// batch_for!($self.state, depolarize1, addrs, *p) -// } -// (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { -// batch_for!($self.state, pauli_error, addrs, *ps) -// } -// (CNOT, TwoQubitBatch(pairs)) => $self.state.cnot_many(pairs), -// (CZ, TwoQubitBatch(pairs)) => $self.state.cz_many(pairs), -// (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { -// $self.state.rxx_many(pairs, *angle) -// } -// (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { -// $self.state.ryy_many(pairs, *angle) -// } -// (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { -// $self.state.rzz_many(pairs, *angle) -// } -// (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { -// batch_pairs_for!($self.state, depolarize2, pairs, *p) -// } -// (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { -// batch_pairs_for!($self.state, two_qubit_pauli_error, pairs, *ps) -// } - -// // Not supported on either backend (Decision 11 + Gate Support -// // Matrix). Loss / CorrelatedLoss handling differs by backend -// // and lives in the caller's impl block, not this macro. -// // Leakage is GeneralizedTableau-only (pinned |0⟩/|1⟩), not a -// // LossyPauliSum qutrit |L⟩ channel. -// (Measure | Reset | Leakage, _) => { -// return Err(eyre!("{} is not supported on the {} backend", $inst, $backend)); -// } - -// (T, Qubit(addr)) => $self.state.rz(*addr, std::f64::consts::PI / 8.0), -// (T, QubitBatch(addrs)) => $self.state.rz_many(addrs, std::f64::consts::PI / 8.0), -// (TAdj, Qubit(addr)) => $self.state.rz(*addr, -std::f64::consts::PI / 8.0), -// (TAdj, QubitBatch(addrs)) => $self.state.rz_many(addrs, -std::f64::consts::PI / 8.0), - -// // U3(θ, φ, λ) = RZ(φ)·RY(θ)·RZ(λ), matching the tableau / Python -// // `u3` (which applies rz(λ); ry(θ); rz(φ)). PauliSum is the -// // Heisenberg picture, so the sub-rotations run in reversed order -// // with the same angles: rz(φ); ry(θ); rz(λ). -// (U3, &QubitU3(addr, theta, phi, lam)) => { -// $self.state.rz(addr, phi); -// $self.state.ry(addr, theta); -// $self.state.rz(addr, lam); -// } - -// (U3, QubitBatchU3(addrs, theta, phi, lam)) => { -// $self.state.rz_many(addrs, *phi); -// $self.state.ry_many(addrs, *theta); -// $self.state.rz_many(addrs, *lam); -// } - -// // Trace: parse the resolved pattern string and compute the -// // trace. Per plan Decision 9, parsing happens on every -// // execution; no module-load caching. -// (Trace, PauliPatternStr(s)) => { -// let pat = PauliPattern::parse(s) -// .map_err(|e| eyre!("invalid Pauli pattern `{}`: {:?}", s, e))?; -// let value = $self.state.trace(&pat); -// return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { -// value, -// }))); -// } - -// // Fallback (mismatched shapes, etc.) -// (inst, msg) => { -// return Err(eyre!( -// "Invalid circuit instruction arguments {:?} for instruction {:?} on the {} backend", -// msg, -// inst, -// $backend -// )); -// } -// }; -// Ok(Effects::None) -// }}; -// } - -// /// PauliSum-backed executor (Heisenberg picture). Holds a `PauliSum` and -// /// answers the same `CircuitInstruction` vocabulary as `CircuitExecutor`, -// /// but without measurement / reset / loss support. -// pub struct PauliSumExecutor> { -// pub state: PauliSum, -// /// Snapshot of the seeded observable, restored by `reset`. -// initial: PauliSum, -// } - -// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -// impl PauliSumExecutor -// where -// T: Config, -// for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, -// { -// fn execute( -// &mut self, -// inst: CircuitInstruction, -// msg: CircuitMessage, -// ) -> Result> { -// self.execute_instruction(&inst, &msg) -// } - -// fn execute_instruction( -// &mut self, -// inst: &CircuitInstruction, -// msg: &CircuitMessage, -// ) -> Result> { -// use CircuitInstruction::*; - -// if matches!(inst, Loss | CorrelatedLoss) { -// return Err(eyre!( -// "{inst} is not supported on the PauliSum backend; use the LossyPauliSum backend instead" -// )); -// } - -// dispatch_common_paulisum!(self, inst, msg, "PauliSum") -// } -// } - -// impl vihaco::Reset for PauliSumExecutor -// where -// T: Config, -// PauliSum: Clone, -// { -// fn reset(&mut self) { -// self.state = self.initial.clone(); -// } -// } - -// /// LossyPauliSum-backed executor. Same dispatch as `PauliSumExecutor` plus -// /// `Loss` / `CorrelatedLoss` channels. The concrete `T` used by the -// /// enclosing `Circuit::LossyPauliSum` variant is a `Config` whose -// /// `PauliWordType` is `LossyPauliWord` (see `LossyPauliSumConfig`). -// pub struct LossyPauliSumExecutor> { -// pub state: PauliSum, -// /// Snapshot of the seeded observable, restored by `reset`. -// initial: PauliSum, -// } - -// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -// impl LossyPauliSumExecutor -// where -// T: Config, -// for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, -// { -// fn execute( -// &mut self, -// inst: CircuitInstruction, -// msg: CircuitMessage, -// ) -> Result> { -// self.execute_instruction(&inst, &msg) -// } - -// fn execute_instruction( -// &mut self, -// inst: &CircuitInstruction, -// msg: &CircuitMessage, -// ) -> Result> { -// use CircuitInstruction::*; -// use CircuitMessage::*; - -// // Loss / CorrelatedLoss are the only instructions that differ from -// // PauliSum; handle them here then delegate everything else to the -// // shared dispatch. -// match (inst, msg) { -// (Loss, &QubitAndFloat(addr, p)) => { -// self.state.loss_channel(addr, p); -// return Ok(Effects::None); -// } -// (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { -// self.state.correlated_loss_channel(*addr0, *addr1, *ps); -// return Ok(Effects::None); -// } -// (Loss, QubitBatchAndFloat(addrs, p)) => { -// batch_for!(self.state, loss_channel, addrs, *p); -// return Ok(Effects::None); -// } -// (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { -// batch_pairs_for!(self.state, correlated_loss_channel, pairs, *ps); -// return Ok(Effects::None); -// } -// _ => {} -// } - -// dispatch_common_paulisum!(self, inst, msg, "LossyPauliSum") -// } -// } - -// impl vihaco::Reset for LossyPauliSumExecutor -// where -// T: Config, -// PauliSum: Clone, -// { -// fn reset(&mut self) { -// self.state = self.initial.clone(); -// } -// } - -// /// Tableau-backed inner enum (Schrödinger picture). Carries the six -// /// size-bucketed `CircuitExecutor` variants; bucket is picked from `n_qubits`. -// pub enum TableauCircuit { -// Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>), -// Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>), -// Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>), -// Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>), -// Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>), -// Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>), -// } - -// impl TableauCircuit { -// pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Result { -// if n_qubits <= 64 { -// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); -// Ok(Self::Bits64(CircuitExecutor { tab })) -// } else if n_qubits <= 128 { -// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); -// Ok(Self::Bits128(CircuitExecutor { tab })) -// } else if n_qubits <= 256 { -// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); -// Ok(Self::Bits256(CircuitExecutor { tab })) -// } else if n_qubits <= 512 { -// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); -// Ok(Self::Bits512(CircuitExecutor { tab })) -// } else if n_qubits <= 1024 { -// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); -// Ok(Self::Bits1024(CircuitExecutor { tab })) -// } else if n_qubits <= MAX_QUBITS { -// let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); -// Ok(Self::Bits2048(CircuitExecutor { tab })) -// } else { -// Err(eyre!( -// "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" -// )) -// } -// } - -// /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a -// /// shot is reproducible. -// pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Result { -// macro_rules! seeded { -// ($variant:ident) => {{ -// let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); -// Ok(Self::$variant(CircuitExecutor { tab })) -// }}; -// } -// if n_qubits <= 64 { -// seeded!(Bits64) -// } else if n_qubits <= 128 { -// seeded!(Bits128) -// } else if n_qubits <= 256 { -// seeded!(Bits256) -// } else if n_qubits <= 512 { -// seeded!(Bits512) -// } else if n_qubits <= 1024 { -// seeded!(Bits1024) -// } else if n_qubits <= MAX_QUBITS { -// seeded!(Bits2048) -// } else { -// Err(eyre!( -// "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" -// )) -// } -// } - -// fn execute_instruction( -// &mut self, -// inst: &CircuitInstruction, -// msg: &CircuitMessage, -// ) -> Result> { -// match self { -// Self::Bits64(ex) => ex.execute_instruction(inst, msg), -// Self::Bits128(ex) => ex.execute_instruction(inst, msg), -// Self::Bits256(ex) => ex.execute_instruction(inst, msg), -// Self::Bits512(ex) => ex.execute_instruction(inst, msg), -// Self::Bits1024(ex) => ex.execute_instruction(inst, msg), -// Self::Bits2048(ex) => ex.execute_instruction(inst, msg), -// } -// } - -// pub fn state_string(&self) -> String { -// match self { -// Self::Bits64(ex) => ex.tab.to_string(), -// Self::Bits128(ex) => ex.tab.to_string(), -// Self::Bits256(ex) => ex.tab.to_string(), -// Self::Bits512(ex) => ex.tab.to_string(), -// Self::Bits1024(ex) => ex.tab.to_string(), -// Self::Bits2048(ex) => ex.tab.to_string(), -// } -// } -// } - -// impl vihaco::Reset for TableauCircuit { -// fn reset(&mut self) { -// match self { -// Self::Bits64(ex) => ex.reset(), -// Self::Bits128(ex) => ex.reset(), -// Self::Bits256(ex) => ex.reset(), -// Self::Bits512(ex) => ex.reset(), -// Self::Bits1024(ex) => ex.reset(), -// Self::Bits2048(ex) => ex.reset(), -// }; -// } -// } - -// /// PauliSum-backed inner enum (Heisenberg picture). Per Decision 7 of the plan, -// /// the size buckets carry `[u8; N]`-storage `ByteFxHashF64` configs (N = 8, 16, -// /// …, 256) rather than the tableau's `[u64; N]` configs; bucket labels match -// /// the semantic qubit count (`Bits64` = 64 qubits) so the outer enum's dispatch -// /// is uniform across backends. -// pub enum PauliSumCircuit { -// Bits64(PauliSumExecutor>), -// Bits128(PauliSumExecutor>), -// Bits256(PauliSumExecutor>), -// Bits512(PauliSumExecutor>), -// Bits1024(PauliSumExecutor>), -// Bits2048(PauliSumExecutor>), -// } - -// impl PauliSumCircuit { -// /// Build a PauliSum-backed circuit, seeding the state with every term: -// /// `for (word, coef) in terms { state += (word, coef); }`. Words must -// /// already be validated against `info.n_qubits` by the caller. -// pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { -// macro_rules! build { -// ($variant:ident, $N:literal) => {{ -// let mut state = PauliSum::>::builder() -// .n_qubits(info.n_qubits) -// .strategy(paulisum_strategy(info)) -// .build(); -// for (word, coef) in terms { -// state += (word.as_str(), *coef); -// } -// let initial = state.clone(); -// Ok(Self::$variant(PauliSumExecutor { state, initial })) -// }}; -// } -// if info.n_qubits <= 64 { -// build!(Bits64, 8) -// } else if info.n_qubits <= 128 { -// build!(Bits128, 16) -// } else if info.n_qubits <= 256 { -// build!(Bits256, 32) -// } else if info.n_qubits <= 512 { -// build!(Bits512, 64) -// } else if info.n_qubits <= 1024 { -// build!(Bits1024, 128) -// } else if info.n_qubits <= MAX_QUBITS { -// build!(Bits2048, 256) -// } else { -// Err(eyre!( -// "cannot simulate {} qubits: maximum is {MAX_QUBITS}", -// info.n_qubits -// )) -// } -// } - -// fn execute_instruction( -// &mut self, -// inst: &CircuitInstruction, -// msg: &CircuitMessage, -// ) -> Result> { -// match self { -// Self::Bits64(ex) => ex.execute_instruction(inst, msg), -// Self::Bits128(ex) => ex.execute_instruction(inst, msg), -// Self::Bits256(ex) => ex.execute_instruction(inst, msg), -// Self::Bits512(ex) => ex.execute_instruction(inst, msg), -// Self::Bits1024(ex) => ex.execute_instruction(inst, msg), -// Self::Bits2048(ex) => ex.execute_instruction(inst, msg), -// } -// } - -// pub fn state_string(&self) -> String { -// match self { -// Self::Bits64(ex) => ex.state.to_string(), -// Self::Bits128(ex) => ex.state.to_string(), -// Self::Bits256(ex) => ex.state.to_string(), -// Self::Bits512(ex) => ex.state.to_string(), -// Self::Bits1024(ex) => ex.state.to_string(), -// Self::Bits2048(ex) => ex.state.to_string(), -// } -// } -// } - -// impl vihaco::Reset for PauliSumCircuit { -// fn reset(&mut self) { -// match self { -// Self::Bits64(ex) => ex.reset(), -// Self::Bits128(ex) => ex.reset(), -// Self::Bits256(ex) => ex.reset(), -// Self::Bits512(ex) => ex.reset(), -// Self::Bits1024(ex) => ex.reset(), -// Self::Bits2048(ex) => ex.reset(), -// }; -// } -// } - -// /// LossyPauliSum-backed inner enum. Identical shape to [`PauliSumCircuit`] -// /// but with `LossyPauliWord`-keyed configs so loss-channel methods dispatch. -// pub enum LossyPauliSumCircuit { -// Bits64(LossyPauliSumExecutor>), -// Bits128(LossyPauliSumExecutor>), -// Bits256(LossyPauliSumExecutor>), -// Bits512(LossyPauliSumExecutor>), -// Bits1024(LossyPauliSumExecutor>), -// Bits2048(LossyPauliSumExecutor>), -// } - -// impl LossyPauliSumCircuit { -// /// Build a LossyPauliSum-backed circuit, seeding every term via -// /// `state += (word, coef)`. Words must already be validated against -// /// `info.n_qubits` by the caller. -// pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { -// macro_rules! build { -// ($variant:ident, $N:literal) => {{ -// let mut state = PauliSum::>::builder() -// .n_qubits(info.n_qubits) -// .strategy(paulisum_strategy(info)) -// .build(); -// for (word, coef) in terms { -// state += (word.as_str(), *coef); -// } -// let initial = state.clone(); -// Ok(Self::$variant(LossyPauliSumExecutor { state, initial })) -// }}; -// } -// if info.n_qubits <= 64 { -// build!(Bits64, 8) -// } else if info.n_qubits <= 128 { -// build!(Bits128, 16) -// } else if info.n_qubits <= 256 { -// build!(Bits256, 32) -// } else if info.n_qubits <= 512 { -// build!(Bits512, 64) -// } else if info.n_qubits <= 1024 { -// build!(Bits1024, 128) -// } else if info.n_qubits <= MAX_QUBITS { -// build!(Bits2048, 256) -// } else { -// Err(eyre!( -// "cannot simulate {} qubits: maximum is {MAX_QUBITS}", -// info.n_qubits -// )) -// } -// } - -// fn execute_instruction( -// &mut self, -// inst: &CircuitInstruction, -// msg: &CircuitMessage, -// ) -> Result> { -// match self { -// Self::Bits64(ex) => ex.execute_instruction(inst, msg), -// Self::Bits128(ex) => ex.execute_instruction(inst, msg), -// Self::Bits256(ex) => ex.execute_instruction(inst, msg), -// Self::Bits512(ex) => ex.execute_instruction(inst, msg), -// Self::Bits1024(ex) => ex.execute_instruction(inst, msg), -// Self::Bits2048(ex) => ex.execute_instruction(inst, msg), -// } -// } - -// pub fn state_string(&self) -> String { -// match self { -// Self::Bits64(ex) => ex.state.to_string(), -// Self::Bits128(ex) => ex.state.to_string(), -// Self::Bits256(ex) => ex.state.to_string(), -// Self::Bits512(ex) => ex.state.to_string(), -// Self::Bits1024(ex) => ex.state.to_string(), -// Self::Bits2048(ex) => ex.state.to_string(), -// } -// } -// } - -// impl vihaco::Reset for LossyPauliSumCircuit { -// fn reset(&mut self) { -// match self { -// Self::Bits64(ex) => ex.reset(), -// Self::Bits128(ex) => ex.reset(), -// Self::Bits256(ex) => ex.reset(), -// Self::Bits512(ex) => ex.reset(), -// Self::Bits1024(ex) => ex.reset(), -// Self::Bits2048(ex) => ex.reset(), -// }; -// } -// } - -// /// Outer `Circuit` enum: backend selector. Picks one of the three inner enums -// /// based on `info.backend` at construction time; from there, every per-step -// /// call routes outer → inner → executor. -// pub enum Circuit { -// Tableau(TableauCircuit), -// PauliSum(PauliSumCircuit), -// LossyPauliSum(LossyPauliSumCircuit), -// } - -// impl vihaco::HasInstructionSet for Circuit { -// type Runtime = vihaco_circuit_isa::CircuitInstruction; -// type Syntax = vihaco_circuit_isa::CircuitSurfaceInstruction; -// } - -// #[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -// impl Circuit { -// /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and -// /// `coefficient_threshold` from `info`; no observable required. -// pub fn tableau(info: &PPVMDeviceInfo) -> Result { -// Ok(Self::Tableau(TableauCircuit::new( -// info.n_qubits, -// info.coefficient_threshold, -// )?)) -// } - -// /// Same as [`Circuit::tableau`], but seed the tableau RNG deterministically -// /// so a shot is reproducible. -// pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { -// Ok(Self::Tableau(TableauCircuit::new_with_seed( -// info.n_qubits, -// info.coefficient_threshold, -// seed, -// )?)) -// } - -// /// Build a PauliSum-backed circuit, seeding the state with every term in -// /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the -// /// caller is responsible for having parsed/validated the words against -// /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). -// pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { -// Ok(Self::PauliSum(PauliSumCircuit::new(info, terms)?)) -// } - -// /// Build a LossyPauliSum-backed circuit. Same contract as -// /// [`Circuit::paulisum`]. -// pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { -// Ok(Self::LossyPauliSum(LossyPauliSumCircuit::new(info, terms)?)) -// } - -// fn execute( -// &mut self, -// inst: CircuitInstruction, -// msg: CircuitMessage, -// ) -> Result> { -// self.execute_instruction(&inst, &msg) -// } - -// fn execute_instruction( -// &mut self, -// inst: &CircuitInstruction, -// msg: &CircuitMessage, -// ) -> Result> { -// match self { -// Self::Tableau(c) => c.execute_instruction(inst, msg), -// Self::PauliSum(c) => c.execute_instruction(inst, msg), -// Self::LossyPauliSum(c) => c.execute_instruction(inst, msg), -// } -// } - -// /// Render the current state. Used by the REPL's `show` command. -// pub fn state_string(&self) -> String { -// match self { -// Self::Tableau(c) => c.state_string(), -// Self::PauliSum(c) => c.state_string(), -// Self::LossyPauliSum(c) => c.state_string(), -// } -// } -// } - -// #[observe(CircuitEffect, effect=CircuitOutcomeEffect)] -// impl Circuit { -// fn observe_circuit_effect( -// &mut self, -// effect: &CircuitEffect, -// ) -> Result> { -// self.execute_instruction(&effect.inst, &effect.msg) -// } -// } - -// impl vihaco::Reset for Circuit { -// fn reset(&mut self) { -// match self { -// Self::Tableau(c) => c.reset(), -// Self::PauliSum(c) => c.reset(), -// Self::LossyPauliSum(c) => c.reset(), -// }; -// } -// } - -// impl Default for Circuit { -// fn default() -> Self { -// // Default backend is Tableau with 0 qubits, which always fits the -// // smallest bucket, so construction here is infallible. -// Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") -// } -// } - // #[cfg(test)] // mod tests { // use super::*; diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 4e9e1a6a7..ec19595fe 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -3,8 +3,8 @@ // pub mod bytecode; pub mod component; -// pub mod composite; pub mod device_info; +pub mod machine; pub mod measurements; pub mod observable; // pub mod shots; diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index fe9d451cb..ed8808914 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -1,584 +1,584 @@ -// SPDX-FileCopyrightText: 2026 The PPVM Authors -// SPDX-License-Identifier: Apache-2.0 - -//! End-to-end fixture coverage: parse + resolve + run each `.sst` file in -//! this directory via the public `PPVM` API. - -use ppvm_vihaco::composite::PPVM; -use ppvm_vihaco::measurements::MeasurementOutcome; - -/// Dump a fixture to a `.ssb` file, load it back, and run it. Exercises the -/// full bytecode round-trip through disk: `dump_file` → `load_bytecode_file`. -fn dump_load_run(sst_path: &str, ssb_name: &str) -> PPVM { - let out = std::env::temp_dir().join(ssb_name); - let out = out.to_str().expect("utf-8 temp path"); - ppvm_vihaco::dump_file(sst_path, out).unwrap_or_else(|e| panic!("dump {sst_path}: {e:?}")); - - let mut machine = PPVM::default(); - machine - .load_bytecode_file(out) - .unwrap_or_else(|e| panic!("load {out}: {e:?}")); - machine.run().unwrap_or_else(|e| panic!("run {out}: {e:?}")); - - let _ = std::fs::remove_file(out); - machine -} - -#[test] -fn bell_sst_runs_and_records_two_measurements() { - let mut machine = PPVM::default(); - machine - .run_file("tests/bell.sst") - .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); - assert_eq!(machine.measurement_record().len(), 2); -} - -#[test] -fn hello_circuit_sst_parses_and_runs() { - let mut machine = PPVM::default(); - machine - .run_file("tests/hello_circuit.sst") - .unwrap_or_else(|e| panic!("run hello_circuit.sst: {e:?}")); - // hello_circuit.sst applies H + CNOT + RX(0.1); no measurements. - assert_eq!(machine.measurement_record().len(), 0); -} - -#[test] -fn arithmetic_sst_uses_lhs_then_rhs_order() { - let machine = ppvm_vihaco::run_file("tests/arithmetic.sst") - .unwrap_or_else(|e| panic!("run arithmetic.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1, "expected exactly one measurement"); - assert_eq!( - record[0].as_slice(), - &[MeasurementOutcome::Zero], - "all arithmetic checks should pass without flipping q0" - ); -} - -#[test] -fn rotxy_sst_runs_and_flips_qubit() { - // `rotxy.sst` applies R(axis_angle=π/2, θ=π) = RY(π) to q0, deterministically - // sending |0> → |1>, then measures it. Exercises the `circuit::circuit.r` path end to - // end: parse → resolve (pop θ, axis_angle, qubit) → execute via `tab.r`. - let machine = - ppvm_vihaco::run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1, "expected exactly one measurement"); - assert_eq!( - record[0].as_slice(), - &[MeasurementOutcome::One], - "R(π/2, π) = RY(π) must flip q0 to 1" - ); -} - -#[test] -fn dumped_rotxy_runs_and_flips_qubit() { - // Same program, but through the bytecode round-trip: confirms the `R` - // instruction survives dump → `.ssb` → load → execute. - let machine = dump_load_run("tests/rotxy.sst", "ppvm_dump_rotxy.ssb"); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1); - assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); -} - -#[test] -fn tableau_leakage_pins_to_one_and_skips_x() { - // `tableau_leakage.sst` leaks q0 with (p0, p1) = (0, 1), pinning |1⟩, - // then applies X. A leaked qubit is frozen, so measure still returns 1. - let machine = ppvm_vihaco::run_file("tests/tableau_leakage.sst") - .unwrap_or_else(|e| panic!("run tableau_leakage.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1, "expected exactly one measurement"); - assert_eq!( - record[0].as_slice(), - &[MeasurementOutcome::One], - "leaked q0 must measure the pinned 1, not LOST, and X must be a no-op" - ); -} - -#[test] -fn dumped_tableau_leakage_pins_to_one() { - let machine = dump_load_run("tests/tableau_leakage.sst", "ppvm_dump_tableau_leakage.ssb"); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1); - assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); -} - -#[test] -fn run_file_via_library_helper() { - let machine = - ppvm_vihaco::run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); - assert_eq!(machine.measurement_record().len(), 2); -} - -#[test] -fn function_call_jumps_into_callee_body() { - // `function_call.sst` has main `call` into `@run_circuit`, which puts q1 - // in |+>, measures it, and `halt`s. Verifies CallPatch resolves the - // symbolic target and op_call actually transfers control there. - let machine = ppvm_vihaco::run_file("tests/function_call.sst") - .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1, "expected exactly one measurement"); - assert_eq!(record[0].len(), 1); - assert!( - record[0][0] != MeasurementOutcome::Lost, - "measurement should not be lost" - ); -} - -#[test] -fn function_call_returns() { - let machine = ppvm_vihaco::run_file("tests/function_call_ret.sst") - .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1, "expected exactly one measurement"); - assert_eq!(record[0].len(), 1); - assert!( - record[0][0] != MeasurementOutcome::Lost, - "measurement should not be lost" - ); -} - -#[test] -fn branch_on_outcome_deterministic_x_path() { - // `branch_on_outcome_x.sst` applies X to q0 instead of H, so the outcome - // is deterministically 1. The cpu::cpu.cond_br must therefore take the @one path, - // which flips q1 before measuring it, yielding m1 = 1 as well. - let machine = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") - .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 2, "expected exactly two measurements"); - assert_eq!( - record[0].as_slice(), - &[MeasurementOutcome::One], - "X-prepared q0 must measure 1" - ); - assert_eq!( - record[1].as_slice(), - &[MeasurementOutcome::One], - "branch must have flipped q1" - ); -} - -#[test] -fn branch_on_outcome_statistics_balanced_and_invariant_holds() { - // `branch_on_outcome.sst` puts q0 in |+>, so its measurement is a fair - // coin. The branch then flips q1 iff the outcome was 1, making m1 == m0 - // an invariant on every shot. Run many shots and check both properties. - const SHOTS: usize = 400; - let mut ones = 0usize; - for _ in 0..SHOTS { - let machine = ppvm_vihaco::run_file("tests/branch_on_outcome.sst") - .unwrap_or_else(|e| panic!("run branch_on_outcome.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 2); - let m0 = record[0][0]; - let m1 = record[1][0]; - assert_eq!(m0, m1, "branch must steer q1 to match q0 on every shot"); - assert!( - m0 != MeasurementOutcome::Lost, - "measurement should not be lost" - ); - if m0 == MeasurementOutcome::One { - ones += 1; - } - } - // Fair coin with SHOTS=400: mean=200, stddev=10. ±6σ window catches a - // truly broken RNG without flaking on a healthy one. - let lo = SHOTS / 2 - 60; - let hi = SHOTS / 2 + 60; - assert!( - (lo..=hi).contains(&ones), - "expected {lo}..={hi} ones out of {SHOTS}, got {ones}" - ); -} - -#[test] -fn function_call_branch_on_both_returned_values() { - // `function_call_branch_both.sst`: helper returns the tri-state outcome - // (0/1/Lost) via `ret 1`. Main first branches on is_lost, then on the - // 0/1 outcome, steering q1 to |1> on the lost path and on the - // kept-outcome=1 path, leaving q1 in |0> only on the kept-outcome=0 - // path. With loss prob 0.5 and a |+> prep: - // P(m1 = 1) = P(lost) + P(kept) · P(outcome = 1 | kept) - // = 0.5 + 0.5 · 0.5 = 0.75 - // P(m0 = lost) = 0.5 - const SHOTS: usize = 400; - let mut q0_lost = 0usize; - let mut q1_ones = 0usize; - for _ in 0..SHOTS { - let machine = ppvm_vihaco::run_file("tests/function_call_branch_both.sst") - .unwrap_or_else(|e| panic!("run function_call_branch_both.sst: {e:?}")); - let record = machine.measurement_record(); - assert_eq!(record.len(), 2, "expected exactly two measurements"); - assert_eq!(record[0].len(), 1); - assert_eq!(record[1].len(), 1); - if record[0][0] == MeasurementOutcome::Lost { - q0_lost += 1; - } - if record[1][0] == MeasurementOutcome::One { - q1_ones += 1; - } - } - // P(lost) = 0.5, SHOTS=400: mean=200, stddev=10. ±6σ window. - assert!( - (140..=260).contains(&q0_lost), - "expected ~200 lost shots, got {q0_lost}" - ); - // P(m1=1) = 0.75, SHOTS=400: mean=300, stddev≈8.66. ±6σ → ~248..352. - assert!( - (240..=360).contains(&q1_ones), - "expected ~300 q1=true shots, got {q1_ones}" - ); -} - -// ─── Task 12: PauliSum / LossyPauliSum end-to-end via .sst source ──────── - -#[test] -fn paulisum_bell_zz_trace_through_sst() { - // Bell-state ⟨ZZ⟩ via PauliSum. Textbook circuit H(0); CNOT(0,1) is - // emitted reversed for Heisenberg propagation: `circuit::circuit.cnot; circuit::circuit.h`. - // Conjugating ZZ by CNOT(0,1) gives Z_1 (= IZ); H on q0 leaves IZ - // untouched. Tracing against |00> matches IZ (pattern `Z?*`) and - // returns +1.0 — matching ⟨Φ+|ZZ|Φ+⟩ = 1. - let machine = ppvm_vihaco::run_file("tests/paulisum_bell_trace.sst") - .unwrap_or_else(|e| panic!("run paulisum_bell_trace.sst: {e:?}")); - let trace = machine.trace_record(); - assert_eq!(trace.len(), 1, "expected one trace emission"); - assert!( - (trace[0] - 1.0).abs() < 1e-12, - "expected ⟨ZZ⟩ = 1.0, got {}", - trace[0] - ); -} - -#[test] -fn paulisum_multi_term_observable_trace_through_sst() { - // Phase 3 wiring: a 2-term observable `1.0*ZZ+0.5*XX` parses on the - // header, seeds both terms, and an end-of-program trace picks up their - // contributions. With no gates applied, tracing `[XZ]?*` matches both - // ZZ and XX directly → coef sum = 1.0 + 0.5 = 1.5. - let machine = ppvm_vihaco::run_file("tests/paulisum_multi_term_trace.sst") - .unwrap_or_else(|e| panic!("run paulisum_multi_term_trace.sst: {e:?}")); - let trace = machine.trace_record(); - assert_eq!(trace.len(), 1); - assert!( - (trace[0] - 1.5).abs() < 1e-12, - "expected 1.5, got {}", - trace[0] - ); -} - -#[test] -fn paulisum_trotter_matches_pure_rust_reference() { - // Two Trotter layers of RXX(0.1) + RZZ(0.05), interleaved with explicit - // `circuit::circuit.truncate`. The .sst-driven path should agree bit-for-bit with a - // pure Rust PauliSum running the same gates: `indexmap::ByteFxHashF64` - // gives deterministic iteration order (Decision 7), so truncation order - // and float accumulation are stable across both paths. - - let machine = ppvm_vihaco::run_file("tests/paulisum_trotter_truncate.sst") - .unwrap_or_else(|e| panic!("run paulisum_trotter_truncate.sst: {e:?}")); - let through_sst = machine.trace_record(); - assert_eq!(through_sst.len(), 1, "expected one trace emission"); - - // Pure Rust reference: same N=8 / strategy / circuit order as the PauliSum - // Bits64 bucket in `ppvm_vihaco::component`. - use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; - use ppvm_pauli_sum::prelude::*; - use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; - type RefConfig = ByteFxHashF64<8, CombinedStrategy>; - - let mut state: PauliSum = PauliSum::builder() - .n_qubits(2) - .strategy(CombinedStrategy( - CoefficientThreshold(1e-6), - MaxPauliWeight(usize::MAX), - )) - .build(); - state += ("ZZ", 1.0); - state.rxx(0, 1, 0.1); - state.rzz(0, 1, 0.05); - state.truncate(); - state.rxx(0, 1, 0.1); - state.rzz(0, 1, 0.05); - state.truncate(); - let pat = PauliPattern::parse("Z?*").expect("parse pattern"); - let reference = state.trace(&pat); - - assert_eq!( - through_sst[0], reference, - ".sst-driven trace must match pure Rust reference bit-for-bit" - ); -} - -#[test] -fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { - // End-to-end LossyPauliSum: header seeds ZZ, body applies CNOT(0,1) - // then loss(q0, 0.3), then traces. The .sst-driven trace must agree - // bit-for-bit with a pure-Rust reference using the same Config (a - // `LossyPauliSumConfig<8>` matching the Bits64 bucket) and operations. - let machine = ppvm_vihaco::run_file("tests/lossy_paulisum_loss_trace.sst") - .unwrap_or_else(|e| panic!("run lossy_paulisum_loss_trace.sst: {e:?}")); - let through_sst = machine.trace_record(); - assert_eq!(through_sst.len(), 1, "expected one trace emission"); - - use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; - use ppvm_pauli_sum::prelude::*; - use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; - type RefConfig = ByteFxHashF64< - 8, - CombinedStrategy, - LossyPauliWord<[u8; 8]>, - >; - - let mut state: PauliSum = PauliSum::builder() - .n_qubits(2) - .strategy(CombinedStrategy( - CoefficientThreshold(1e-10), - MaxPauliWeight(usize::MAX), - )) - .build(); - state += ("ZZ", 1.0); - state.cnot(0, 1); - state.loss_channel(0, 0.3); - state.truncate(); - let pat = PauliPattern::parse("Z?*").expect("parse pattern"); - let reference = state.trace(&pat); - - assert_eq!( - through_sst[0], reference, - ".sst-driven trace must match pure Rust reference bit-for-bit" - ); -} - -#[test] -fn paulisum_measure_returns_unsupported_error() { - // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a - // clear "not supported on the PauliSum backend" error. - let mut machine = PPVM::default(); - machine - .load_file("tests/paulisum_measure_error.sst") - .unwrap_or_else(|e| panic!("load paulisum_measure_error.sst: {e:?}")); - let err = machine.run().unwrap_err(); - let msg = err.to_string(); - assert!( - msg.contains("not supported on the PauliSum backend"), - "expected PauliSum-rejection error, got: {msg}" - ); -} - -#[test] -fn paulisum_leakage_returns_unsupported_error() { - // Leakage is tableau-only. PauliSum must reject it the same way it - // rejects Measure — not with a mismatched-argument fallback. - let mut machine = PPVM::default(); - machine - .load_file("tests/paulisum_leakage_error.sst") - .unwrap_or_else(|e| panic!("load paulisum_leakage_error.sst: {e:?}")); - let err = machine.run().unwrap_err(); - let msg = err.to_string(); - assert!( - msg.contains("not supported on the PauliSum backend"), - "expected PauliSum-rejection error, got: {msg}" - ); -} - -// ─── Task 16: Tableau-side Trace, cross-backend agreement ──────────────── - -#[test] -fn tableau_bell_zz_trace_through_sst() { - // Bell-state ⟨ZZ⟩ via the Tableau backend's `Trace` instruction: - // forward H(0); CNOT(0, 1) leaves |ψ⟩ = |Φ+⟩, and `Z0Z1` matches exactly - // the Pauli word ZZ, so `tab.trace(&pat) = ⟨Φ+|ZZ|Φ+⟩ = 1`. - let machine = ppvm_vihaco::run_file("tests/tableau_bell_trace.sst") - .unwrap_or_else(|e| panic!("run tableau_bell_trace.sst: {e:?}")); - let trace = machine.trace_record(); - assert_eq!(trace.len(), 1, "expected one trace emission"); - assert!( - (trace[0] - 1.0).abs() < 1e-12, - "expected ⟨ZZ⟩ = 1.0, got {}", - trace[0] - ); -} - -fn assert_cross_backend_agreement(tableau_sst: &str, paulisum_sst: &str) { - let tab = - ppvm_vihaco::run_file(tableau_sst).unwrap_or_else(|e| panic!("run {tableau_sst}: {e:?}")); - let ps = - ppvm_vihaco::run_file(paulisum_sst).unwrap_or_else(|e| panic!("run {paulisum_sst}: {e:?}")); - let tab_v = tab.trace_record(); - let ps_v = ps.trace_record(); - assert_eq!(tab_v.len(), 1, "{tableau_sst}: expected one trace emission"); - assert_eq!(ps_v.len(), 1, "{paulisum_sst}: expected one trace emission"); - assert!( - (tab_v[0] - ps_v[0]).abs() < 1e-12, - "tableau {} vs PauliSum {} (|Δ|={}) for {tableau_sst} ↔ {paulisum_sst}", - tab_v[0], - ps_v[0], - (tab_v[0] - ps_v[0]).abs() - ); -} - -#[test] -fn tableau_and_paulisum_agree_on_bell_zz_trace() { - // ⟨Φ+|ZZ|Φ+⟩ = 1. Tableau forward-evolves |0…0⟩ and matches `Z0Z1`; - // PauliSum Heisenberg-propagates ZZ backward through the reversed - // circuit, then sums Z/I-only coefficients. - assert_cross_backend_agreement( - "tests/tableau_bell_trace.sst", - "tests/paulisum_bell_trace.sst", - ); -} - -#[test] -fn tableau_and_paulisum_agree_on_ghz_xxx_trace() { - // ⟨GHZ|XXX|GHZ⟩ = 1. Exercises a 3-qubit Clifford chain (H + two - // CNOTs) and the non-trivial Heisenberg evolution XXX → ZII through - // the reversed circuit on the PauliSum side. - assert_cross_backend_agreement( - "tests/tableau_ghz_xxx_trace.sst", - "tests/paulisum_ghz_xxx_trace.sst", - ); -} - -#[test] -fn tableau_and_paulisum_agree_on_ry_z_trace() { - // ⟨RY(θ)|0⟩|Z|RY(θ)|0⟩⟩ = cos(θ). Exercises a non-Clifford rotation: - // the tableau opens a branch via `tab.ry`, while the PauliSum applies - // the Heisenberg dual RY(θ)†·Z·RY(θ) = cos(θ)·Z + sin(θ)·X in one step. - assert_cross_backend_agreement( - "tests/tableau_ry_z_trace.sst", - "tests/paulisum_ry_z_trace.sst", - ); -} - -// ─── Auto-detect via load_file: route by content, not extension ─────────── - -#[test] -fn is_bytecode_distinguishes_ssb_from_sst() { - let sst = std::fs::read("tests/bell.sst").expect("read bell.sst"); - assert!( - !ppvm_vihaco::bytecode::is_bytecode(&sst), - ".sst source must not be detected as bytecode" - ); - - let ssb = ppvm_vihaco::bytecode::compile_to_bytes( - &String::from_utf8(sst).expect("bell.sst is utf-8"), - ) - .expect("compile bell.sst"); - assert!( - ppvm_vihaco::bytecode::is_bytecode(&ssb), - "dumped .ssb must be detected as bytecode" - ); - - // Inputs shorter than the 4-byte magic are never bytecode. Note "PPVM" as - // text also fails: the magic is a little-endian u32, so its on-disk bytes - // are "MVPP", not "PPVM". - assert!(!ppvm_vihaco::bytecode::is_bytecode(b"PPV")); - assert!(!ppvm_vihaco::bytecode::is_bytecode(b"")); - assert!(!ppvm_vihaco::bytecode::is_bytecode(b"PPVM")); -} - -#[test] -fn load_file_auto_detects_bytecode_and_text() { - // Use the deterministic X-prepared fixture: q0 measures 1, the branch - // flips q1, so both routes must yield exactly [1], [1]. Any divergence — - // or a binary file mis-parsed as text — fails loudly. - let from_text = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") - .unwrap_or_else(|e| panic!("run .sst via load_file: {e:?}")); - - // Dump the same fixture to a `.ssb` and run *that file* through the same - // run_file entry point. If load_file didn't sniff the magic it would try - // to parse the binary as text and error. - let out = std::env::temp_dir().join("ppvm_autodetect_branch_x.ssb"); - let out = out.to_str().expect("utf-8 temp path"); - ppvm_vihaco::dump_file("tests/branch_on_outcome_x.sst", out) - .unwrap_or_else(|e| panic!("dump: {e:?}")); - let from_binary = ppvm_vihaco::run_file(out).unwrap_or_else(|e| panic!("run .ssb: {e:?}")); - let _ = std::fs::remove_file(out); - - for (label, machine) in [("text", &from_text), ("binary", &from_binary)] { - let record = machine.measurement_record(); - assert_eq!(record.len(), 2, "{label}: expected two measurements"); - assert_eq!( - record[0].as_slice(), - &[MeasurementOutcome::One], - "{label}: X-prepared q0 must measure 1" - ); - assert_eq!( - record[1].as_slice(), - &[MeasurementOutcome::One], - "{label}: branch must flip q1" - ); - } -} - -// ─── Bytecode round-trip: dump → load → execute each fixture ────────────── - -#[test] -fn dumped_bell_records_two_measurements() { - let machine = dump_load_run("tests/bell.sst", "ppvm_dump_bell.ssb"); - assert_eq!(machine.measurement_record().len(), 2); -} - -#[test] -fn dumped_hello_circuit_runs_with_no_measurements() { - let machine = dump_load_run("tests/hello_circuit.sst", "ppvm_dump_hello_circuit.ssb"); - assert_eq!(machine.measurement_record().len(), 0); -} - -#[test] -fn dumped_function_call_executes_callee() { - let machine = dump_load_run("tests/function_call.sst", "ppvm_dump_function_call.ssb"); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1); - assert_eq!(record[0].len(), 1); - assert!(record[0][0] != MeasurementOutcome::Lost); -} - -#[test] -fn dumped_function_call_ret_executes() { - let machine = dump_load_run( - "tests/function_call_ret.sst", - "ppvm_dump_function_call_ret.ssb", - ); - let record = machine.measurement_record(); - assert_eq!(record.len(), 1); - assert_eq!(record[0].len(), 1); -} - -#[test] -fn dumped_branch_on_outcome_x_is_deterministic() { - // X-prepared q0 measures 1, so the branch flips q1 → both outcomes are 1. - // Confirms branch targets survive the dump/load round-trip. - let machine = dump_load_run("tests/branch_on_outcome_x.sst", "ppvm_dump_branch_x.ssb"); - let record = machine.measurement_record(); - assert_eq!(record.len(), 2); - assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); - assert_eq!(record[1].as_slice(), &[MeasurementOutcome::One]); -} - -#[test] -fn dumped_branch_on_outcome_preserves_invariant() { - // q0 in |+> is a fair coin, but the branch steers q1 to match q0 every - // shot — that invariant must hold after a round-trip. - let machine = dump_load_run("tests/branch_on_outcome.sst", "ppvm_dump_branch.ssb"); - let record = machine.measurement_record(); - assert_eq!(record.len(), 2); - assert_eq!(record[0][0], record[1][0]); -} - -#[test] -fn dumped_function_call_branch_both_runs() { - let machine = dump_load_run( - "tests/function_call_branch_both.sst", - "ppvm_dump_function_call_branch_both.ssb", - ); - let record = machine.measurement_record(); - assert_eq!(record.len(), 2); - assert_eq!(record[0].len(), 1); - assert_eq!(record[1].len(), 1); -} +// // SPDX-FileCopyrightText: 2026 The PPVM Authors +// // SPDX-License-Identifier: Apache-2.0 + +// //! End-to-end fixture coverage: parse + resolve + run each `.sst` file in +// //! this directory via the public `PPVM` API. + +// use ppvm_vihaco::composite::PPVM; +// use ppvm_vihaco::measurements::MeasurementOutcome; + +// /// Dump a fixture to a `.ssb` file, load it back, and run it. Exercises the +// /// full bytecode round-trip through disk: `dump_file` → `load_bytecode_file`. +// fn dump_load_run(sst_path: &str, ssb_name: &str) -> PPVM { +// let out = std::env::temp_dir().join(ssb_name); +// let out = out.to_str().expect("utf-8 temp path"); +// ppvm_vihaco::dump_file(sst_path, out).unwrap_or_else(|e| panic!("dump {sst_path}: {e:?}")); + +// let mut machine = PPVM::default(); +// machine +// .load_bytecode_file(out) +// .unwrap_or_else(|e| panic!("load {out}: {e:?}")); +// machine.run().unwrap_or_else(|e| panic!("run {out}: {e:?}")); + +// let _ = std::fs::remove_file(out); +// machine +// } + +// #[test] +// fn bell_sst_runs_and_records_two_measurements() { +// let mut machine = PPVM::default(); +// machine +// .run_file("tests/bell.sst") +// .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); +// assert_eq!(machine.measurement_record().len(), 2); +// } + +// #[test] +// fn hello_circuit_sst_parses_and_runs() { +// let mut machine = PPVM::default(); +// machine +// .run_file("tests/hello_circuit.sst") +// .unwrap_or_else(|e| panic!("run hello_circuit.sst: {e:?}")); +// // hello_circuit.sst applies H + CNOT + RX(0.1); no measurements. +// assert_eq!(machine.measurement_record().len(), 0); +// } + +// #[test] +// fn arithmetic_sst_uses_lhs_then_rhs_order() { +// let machine = ppvm_vihaco::run_file("tests/arithmetic.sst") +// .unwrap_or_else(|e| panic!("run arithmetic.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1, "expected exactly one measurement"); +// assert_eq!( +// record[0].as_slice(), +// &[MeasurementOutcome::Zero], +// "all arithmetic checks should pass without flipping q0" +// ); +// } + +// #[test] +// fn rotxy_sst_runs_and_flips_qubit() { +// // `rotxy.sst` applies R(axis_angle=π/2, θ=π) = RY(π) to q0, deterministically +// // sending |0> → |1>, then measures it. Exercises the `circuit::circuit.r` path end to +// // end: parse → resolve (pop θ, axis_angle, qubit) → execute via `tab.r`. +// let machine = +// ppvm_vihaco::run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1, "expected exactly one measurement"); +// assert_eq!( +// record[0].as_slice(), +// &[MeasurementOutcome::One], +// "R(π/2, π) = RY(π) must flip q0 to 1" +// ); +// } + +// #[test] +// fn dumped_rotxy_runs_and_flips_qubit() { +// // Same program, but through the bytecode round-trip: confirms the `R` +// // instruction survives dump → `.ssb` → load → execute. +// let machine = dump_load_run("tests/rotxy.sst", "ppvm_dump_rotxy.ssb"); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1); +// assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); +// } + +// #[test] +// fn tableau_leakage_pins_to_one_and_skips_x() { +// // `tableau_leakage.sst` leaks q0 with (p0, p1) = (0, 1), pinning |1⟩, +// // then applies X. A leaked qubit is frozen, so measure still returns 1. +// let machine = ppvm_vihaco::run_file("tests/tableau_leakage.sst") +// .unwrap_or_else(|e| panic!("run tableau_leakage.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1, "expected exactly one measurement"); +// assert_eq!( +// record[0].as_slice(), +// &[MeasurementOutcome::One], +// "leaked q0 must measure the pinned 1, not LOST, and X must be a no-op" +// ); +// } + +// #[test] +// fn dumped_tableau_leakage_pins_to_one() { +// let machine = dump_load_run("tests/tableau_leakage.sst", "ppvm_dump_tableau_leakage.ssb"); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1); +// assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); +// } + +// #[test] +// fn run_file_via_library_helper() { +// let machine = +// ppvm_vihaco::run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); +// assert_eq!(machine.measurement_record().len(), 2); +// } + +// #[test] +// fn function_call_jumps_into_callee_body() { +// // `function_call.sst` has main `call` into `@run_circuit`, which puts q1 +// // in |+>, measures it, and `halt`s. Verifies CallPatch resolves the +// // symbolic target and op_call actually transfers control there. +// let machine = ppvm_vihaco::run_file("tests/function_call.sst") +// .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1, "expected exactly one measurement"); +// assert_eq!(record[0].len(), 1); +// assert!( +// record[0][0] != MeasurementOutcome::Lost, +// "measurement should not be lost" +// ); +// } + +// #[test] +// fn function_call_returns() { +// let machine = ppvm_vihaco::run_file("tests/function_call_ret.sst") +// .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1, "expected exactly one measurement"); +// assert_eq!(record[0].len(), 1); +// assert!( +// record[0][0] != MeasurementOutcome::Lost, +// "measurement should not be lost" +// ); +// } + +// #[test] +// fn branch_on_outcome_deterministic_x_path() { +// // `branch_on_outcome_x.sst` applies X to q0 instead of H, so the outcome +// // is deterministically 1. The cpu::cpu.cond_br must therefore take the @one path, +// // which flips q1 before measuring it, yielding m1 = 1 as well. +// let machine = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") +// .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2, "expected exactly two measurements"); +// assert_eq!( +// record[0].as_slice(), +// &[MeasurementOutcome::One], +// "X-prepared q0 must measure 1" +// ); +// assert_eq!( +// record[1].as_slice(), +// &[MeasurementOutcome::One], +// "branch must have flipped q1" +// ); +// } + +// #[test] +// fn branch_on_outcome_statistics_balanced_and_invariant_holds() { +// // `branch_on_outcome.sst` puts q0 in |+>, so its measurement is a fair +// // coin. The branch then flips q1 iff the outcome was 1, making m1 == m0 +// // an invariant on every shot. Run many shots and check both properties. +// const SHOTS: usize = 400; +// let mut ones = 0usize; +// for _ in 0..SHOTS { +// let machine = ppvm_vihaco::run_file("tests/branch_on_outcome.sst") +// .unwrap_or_else(|e| panic!("run branch_on_outcome.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2); +// let m0 = record[0][0]; +// let m1 = record[1][0]; +// assert_eq!(m0, m1, "branch must steer q1 to match q0 on every shot"); +// assert!( +// m0 != MeasurementOutcome::Lost, +// "measurement should not be lost" +// ); +// if m0 == MeasurementOutcome::One { +// ones += 1; +// } +// } +// // Fair coin with SHOTS=400: mean=200, stddev=10. ±6σ window catches a +// // truly broken RNG without flaking on a healthy one. +// let lo = SHOTS / 2 - 60; +// let hi = SHOTS / 2 + 60; +// assert!( +// (lo..=hi).contains(&ones), +// "expected {lo}..={hi} ones out of {SHOTS}, got {ones}" +// ); +// } + +// #[test] +// fn function_call_branch_on_both_returned_values() { +// // `function_call_branch_both.sst`: helper returns the tri-state outcome +// // (0/1/Lost) via `ret 1`. Main first branches on is_lost, then on the +// // 0/1 outcome, steering q1 to |1> on the lost path and on the +// // kept-outcome=1 path, leaving q1 in |0> only on the kept-outcome=0 +// // path. With loss prob 0.5 and a |+> prep: +// // P(m1 = 1) = P(lost) + P(kept) · P(outcome = 1 | kept) +// // = 0.5 + 0.5 · 0.5 = 0.75 +// // P(m0 = lost) = 0.5 +// const SHOTS: usize = 400; +// let mut q0_lost = 0usize; +// let mut q1_ones = 0usize; +// for _ in 0..SHOTS { +// let machine = ppvm_vihaco::run_file("tests/function_call_branch_both.sst") +// .unwrap_or_else(|e| panic!("run function_call_branch_both.sst: {e:?}")); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2, "expected exactly two measurements"); +// assert_eq!(record[0].len(), 1); +// assert_eq!(record[1].len(), 1); +// if record[0][0] == MeasurementOutcome::Lost { +// q0_lost += 1; +// } +// if record[1][0] == MeasurementOutcome::One { +// q1_ones += 1; +// } +// } +// // P(lost) = 0.5, SHOTS=400: mean=200, stddev=10. ±6σ window. +// assert!( +// (140..=260).contains(&q0_lost), +// "expected ~200 lost shots, got {q0_lost}" +// ); +// // P(m1=1) = 0.75, SHOTS=400: mean=300, stddev≈8.66. ±6σ → ~248..352. +// assert!( +// (240..=360).contains(&q1_ones), +// "expected ~300 q1=true shots, got {q1_ones}" +// ); +// } + +// // ─── Task 12: PauliSum / LossyPauliSum end-to-end via .sst source ──────── + +// #[test] +// fn paulisum_bell_zz_trace_through_sst() { +// // Bell-state ⟨ZZ⟩ via PauliSum. Textbook circuit H(0); CNOT(0,1) is +// // emitted reversed for Heisenberg propagation: `circuit::circuit.cnot; circuit::circuit.h`. +// // Conjugating ZZ by CNOT(0,1) gives Z_1 (= IZ); H on q0 leaves IZ +// // untouched. Tracing against |00> matches IZ (pattern `Z?*`) and +// // returns +1.0 — matching ⟨Φ+|ZZ|Φ+⟩ = 1. +// let machine = ppvm_vihaco::run_file("tests/paulisum_bell_trace.sst") +// .unwrap_or_else(|e| panic!("run paulisum_bell_trace.sst: {e:?}")); +// let trace = machine.trace_record(); +// assert_eq!(trace.len(), 1, "expected one trace emission"); +// assert!( +// (trace[0] - 1.0).abs() < 1e-12, +// "expected ⟨ZZ⟩ = 1.0, got {}", +// trace[0] +// ); +// } + +// #[test] +// fn paulisum_multi_term_observable_trace_through_sst() { +// // Phase 3 wiring: a 2-term observable `1.0*ZZ+0.5*XX` parses on the +// // header, seeds both terms, and an end-of-program trace picks up their +// // contributions. With no gates applied, tracing `[XZ]?*` matches both +// // ZZ and XX directly → coef sum = 1.0 + 0.5 = 1.5. +// let machine = ppvm_vihaco::run_file("tests/paulisum_multi_term_trace.sst") +// .unwrap_or_else(|e| panic!("run paulisum_multi_term_trace.sst: {e:?}")); +// let trace = machine.trace_record(); +// assert_eq!(trace.len(), 1); +// assert!( +// (trace[0] - 1.5).abs() < 1e-12, +// "expected 1.5, got {}", +// trace[0] +// ); +// } + +// #[test] +// fn paulisum_trotter_matches_pure_rust_reference() { +// // Two Trotter layers of RXX(0.1) + RZZ(0.05), interleaved with explicit +// // `circuit::circuit.truncate`. The .sst-driven path should agree bit-for-bit with a +// // pure Rust PauliSum running the same gates: `indexmap::ByteFxHashF64` +// // gives deterministic iteration order (Decision 7), so truncation order +// // and float accumulation are stable across both paths. + +// let machine = ppvm_vihaco::run_file("tests/paulisum_trotter_truncate.sst") +// .unwrap_or_else(|e| panic!("run paulisum_trotter_truncate.sst: {e:?}")); +// let through_sst = machine.trace_record(); +// assert_eq!(through_sst.len(), 1, "expected one trace emission"); + +// // Pure Rust reference: same N=8 / strategy / circuit order as the PauliSum +// // Bits64 bucket in `ppvm_vihaco::component`. +// use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; +// use ppvm_pauli_sum::prelude::*; +// use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; +// type RefConfig = ByteFxHashF64<8, CombinedStrategy>; + +// let mut state: PauliSum = PauliSum::builder() +// .n_qubits(2) +// .strategy(CombinedStrategy( +// CoefficientThreshold(1e-6), +// MaxPauliWeight(usize::MAX), +// )) +// .build(); +// state += ("ZZ", 1.0); +// state.rxx(0, 1, 0.1); +// state.rzz(0, 1, 0.05); +// state.truncate(); +// state.rxx(0, 1, 0.1); +// state.rzz(0, 1, 0.05); +// state.truncate(); +// let pat = PauliPattern::parse("Z?*").expect("parse pattern"); +// let reference = state.trace(&pat); + +// assert_eq!( +// through_sst[0], reference, +// ".sst-driven trace must match pure Rust reference bit-for-bit" +// ); +// } + +// #[test] +// fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { +// // End-to-end LossyPauliSum: header seeds ZZ, body applies CNOT(0,1) +// // then loss(q0, 0.3), then traces. The .sst-driven trace must agree +// // bit-for-bit with a pure-Rust reference using the same Config (a +// // `LossyPauliSumConfig<8>` matching the Bits64 bucket) and operations. +// let machine = ppvm_vihaco::run_file("tests/lossy_paulisum_loss_trace.sst") +// .unwrap_or_else(|e| panic!("run lossy_paulisum_loss_trace.sst: {e:?}")); +// let through_sst = machine.trace_record(); +// assert_eq!(through_sst.len(), 1, "expected one trace emission"); + +// use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; +// use ppvm_pauli_sum::prelude::*; +// use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; +// type RefConfig = ByteFxHashF64< +// 8, +// CombinedStrategy, +// LossyPauliWord<[u8; 8]>, +// >; + +// let mut state: PauliSum = PauliSum::builder() +// .n_qubits(2) +// .strategy(CombinedStrategy( +// CoefficientThreshold(1e-10), +// MaxPauliWeight(usize::MAX), +// )) +// .build(); +// state += ("ZZ", 1.0); +// state.cnot(0, 1); +// state.loss_channel(0, 0.3); +// state.truncate(); +// let pat = PauliPattern::parse("Z?*").expect("parse pattern"); +// let reference = state.trace(&pat); + +// assert_eq!( +// through_sst[0], reference, +// ".sst-driven trace must match pure Rust reference bit-for-bit" +// ); +// } + +// #[test] +// fn paulisum_measure_returns_unsupported_error() { +// // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a +// // clear "not supported on the PauliSum backend" error. +// let mut machine = PPVM::default(); +// machine +// .load_file("tests/paulisum_measure_error.sst") +// .unwrap_or_else(|e| panic!("load paulisum_measure_error.sst: {e:?}")); +// let err = machine.run().unwrap_err(); +// let msg = err.to_string(); +// assert!( +// msg.contains("not supported on the PauliSum backend"), +// "expected PauliSum-rejection error, got: {msg}" +// ); +// } + +// #[test] +// fn paulisum_leakage_returns_unsupported_error() { +// // Leakage is tableau-only. PauliSum must reject it the same way it +// // rejects Measure — not with a mismatched-argument fallback. +// let mut machine = PPVM::default(); +// machine +// .load_file("tests/paulisum_leakage_error.sst") +// .unwrap_or_else(|e| panic!("load paulisum_leakage_error.sst: {e:?}")); +// let err = machine.run().unwrap_err(); +// let msg = err.to_string(); +// assert!( +// msg.contains("not supported on the PauliSum backend"), +// "expected PauliSum-rejection error, got: {msg}" +// ); +// } + +// // ─── Task 16: Tableau-side Trace, cross-backend agreement ──────────────── + +// #[test] +// fn tableau_bell_zz_trace_through_sst() { +// // Bell-state ⟨ZZ⟩ via the Tableau backend's `Trace` instruction: +// // forward H(0); CNOT(0, 1) leaves |ψ⟩ = |Φ+⟩, and `Z0Z1` matches exactly +// // the Pauli word ZZ, so `tab.trace(&pat) = ⟨Φ+|ZZ|Φ+⟩ = 1`. +// let machine = ppvm_vihaco::run_file("tests/tableau_bell_trace.sst") +// .unwrap_or_else(|e| panic!("run tableau_bell_trace.sst: {e:?}")); +// let trace = machine.trace_record(); +// assert_eq!(trace.len(), 1, "expected one trace emission"); +// assert!( +// (trace[0] - 1.0).abs() < 1e-12, +// "expected ⟨ZZ⟩ = 1.0, got {}", +// trace[0] +// ); +// } + +// fn assert_cross_backend_agreement(tableau_sst: &str, paulisum_sst: &str) { +// let tab = +// ppvm_vihaco::run_file(tableau_sst).unwrap_or_else(|e| panic!("run {tableau_sst}: {e:?}")); +// let ps = +// ppvm_vihaco::run_file(paulisum_sst).unwrap_or_else(|e| panic!("run {paulisum_sst}: {e:?}")); +// let tab_v = tab.trace_record(); +// let ps_v = ps.trace_record(); +// assert_eq!(tab_v.len(), 1, "{tableau_sst}: expected one trace emission"); +// assert_eq!(ps_v.len(), 1, "{paulisum_sst}: expected one trace emission"); +// assert!( +// (tab_v[0] - ps_v[0]).abs() < 1e-12, +// "tableau {} vs PauliSum {} (|Δ|={}) for {tableau_sst} ↔ {paulisum_sst}", +// tab_v[0], +// ps_v[0], +// (tab_v[0] - ps_v[0]).abs() +// ); +// } + +// #[test] +// fn tableau_and_paulisum_agree_on_bell_zz_trace() { +// // ⟨Φ+|ZZ|Φ+⟩ = 1. Tableau forward-evolves |0…0⟩ and matches `Z0Z1`; +// // PauliSum Heisenberg-propagates ZZ backward through the reversed +// // circuit, then sums Z/I-only coefficients. +// assert_cross_backend_agreement( +// "tests/tableau_bell_trace.sst", +// "tests/paulisum_bell_trace.sst", +// ); +// } + +// #[test] +// fn tableau_and_paulisum_agree_on_ghz_xxx_trace() { +// // ⟨GHZ|XXX|GHZ⟩ = 1. Exercises a 3-qubit Clifford chain (H + two +// // CNOTs) and the non-trivial Heisenberg evolution XXX → ZII through +// // the reversed circuit on the PauliSum side. +// assert_cross_backend_agreement( +// "tests/tableau_ghz_xxx_trace.sst", +// "tests/paulisum_ghz_xxx_trace.sst", +// ); +// } + +// #[test] +// fn tableau_and_paulisum_agree_on_ry_z_trace() { +// // ⟨RY(θ)|0⟩|Z|RY(θ)|0⟩⟩ = cos(θ). Exercises a non-Clifford rotation: +// // the tableau opens a branch via `tab.ry`, while the PauliSum applies +// // the Heisenberg dual RY(θ)†·Z·RY(θ) = cos(θ)·Z + sin(θ)·X in one step. +// assert_cross_backend_agreement( +// "tests/tableau_ry_z_trace.sst", +// "tests/paulisum_ry_z_trace.sst", +// ); +// } + +// // ─── Auto-detect via load_file: route by content, not extension ─────────── + +// #[test] +// fn is_bytecode_distinguishes_ssb_from_sst() { +// let sst = std::fs::read("tests/bell.sst").expect("read bell.sst"); +// assert!( +// !ppvm_vihaco::bytecode::is_bytecode(&sst), +// ".sst source must not be detected as bytecode" +// ); + +// let ssb = ppvm_vihaco::bytecode::compile_to_bytes( +// &String::from_utf8(sst).expect("bell.sst is utf-8"), +// ) +// .expect("compile bell.sst"); +// assert!( +// ppvm_vihaco::bytecode::is_bytecode(&ssb), +// "dumped .ssb must be detected as bytecode" +// ); + +// // Inputs shorter than the 4-byte magic are never bytecode. Note "PPVM" as +// // text also fails: the magic is a little-endian u32, so its on-disk bytes +// // are "MVPP", not "PPVM". +// assert!(!ppvm_vihaco::bytecode::is_bytecode(b"PPV")); +// assert!(!ppvm_vihaco::bytecode::is_bytecode(b"")); +// assert!(!ppvm_vihaco::bytecode::is_bytecode(b"PPVM")); +// } + +// #[test] +// fn load_file_auto_detects_bytecode_and_text() { +// // Use the deterministic X-prepared fixture: q0 measures 1, the branch +// // flips q1, so both routes must yield exactly [1], [1]. Any divergence — +// // or a binary file mis-parsed as text — fails loudly. +// let from_text = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") +// .unwrap_or_else(|e| panic!("run .sst via load_file: {e:?}")); + +// // Dump the same fixture to a `.ssb` and run *that file* through the same +// // run_file entry point. If load_file didn't sniff the magic it would try +// // to parse the binary as text and error. +// let out = std::env::temp_dir().join("ppvm_autodetect_branch_x.ssb"); +// let out = out.to_str().expect("utf-8 temp path"); +// ppvm_vihaco::dump_file("tests/branch_on_outcome_x.sst", out) +// .unwrap_or_else(|e| panic!("dump: {e:?}")); +// let from_binary = ppvm_vihaco::run_file(out).unwrap_or_else(|e| panic!("run .ssb: {e:?}")); +// let _ = std::fs::remove_file(out); + +// for (label, machine) in [("text", &from_text), ("binary", &from_binary)] { +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2, "{label}: expected two measurements"); +// assert_eq!( +// record[0].as_slice(), +// &[MeasurementOutcome::One], +// "{label}: X-prepared q0 must measure 1" +// ); +// assert_eq!( +// record[1].as_slice(), +// &[MeasurementOutcome::One], +// "{label}: branch must flip q1" +// ); +// } +// } + +// // ─── Bytecode round-trip: dump → load → execute each fixture ────────────── + +// #[test] +// fn dumped_bell_records_two_measurements() { +// let machine = dump_load_run("tests/bell.sst", "ppvm_dump_bell.ssb"); +// assert_eq!(machine.measurement_record().len(), 2); +// } + +// #[test] +// fn dumped_hello_circuit_runs_with_no_measurements() { +// let machine = dump_load_run("tests/hello_circuit.sst", "ppvm_dump_hello_circuit.ssb"); +// assert_eq!(machine.measurement_record().len(), 0); +// } + +// #[test] +// fn dumped_function_call_executes_callee() { +// let machine = dump_load_run("tests/function_call.sst", "ppvm_dump_function_call.ssb"); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1); +// assert_eq!(record[0].len(), 1); +// assert!(record[0][0] != MeasurementOutcome::Lost); +// } + +// #[test] +// fn dumped_function_call_ret_executes() { +// let machine = dump_load_run( +// "tests/function_call_ret.sst", +// "ppvm_dump_function_call_ret.ssb", +// ); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1); +// assert_eq!(record[0].len(), 1); +// } + +// #[test] +// fn dumped_branch_on_outcome_x_is_deterministic() { +// // X-prepared q0 measures 1, so the branch flips q1 → both outcomes are 1. +// // Confirms branch targets survive the dump/load round-trip. +// let machine = dump_load_run("tests/branch_on_outcome_x.sst", "ppvm_dump_branch_x.ssb"); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2); +// assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); +// assert_eq!(record[1].as_slice(), &[MeasurementOutcome::One]); +// } + +// #[test] +// fn dumped_branch_on_outcome_preserves_invariant() { +// // q0 in |+> is a fair coin, but the branch steers q1 to match q0 every +// // shot — that invariant must hold after a round-trip. +// let machine = dump_load_run("tests/branch_on_outcome.sst", "ppvm_dump_branch.ssb"); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2); +// assert_eq!(record[0][0], record[1][0]); +// } + +// #[test] +// fn dumped_function_call_branch_both_runs() { +// let machine = dump_load_run( +// "tests/function_call_branch_both.sst", +// "ppvm_dump_function_call_branch_both.ssb", +// ); +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2); +// assert_eq!(record[0].len(), 1); +// assert_eq!(record[1].len(), 1); +// } From dd78d470d789302008f7a136ba16ff4d9879f14b Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 3 Nov 2026 15:47:21 +0100 Subject: [PATCH 05/36] Add PauliSum execute impls --- crates/ppvm-vihaco/src/component.rs | 302 ++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index cee35a059..cc1cdc77d 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -362,6 +362,308 @@ where } } +/// PauliSum-backed executor (Heisenberg picture). Answers the same `circuit` +/// dialect as `CircuitExecutor`, but without measurement / reset / loss support. +#[component] +#[instructions { circuit }] +pub struct PauliSumExecutor +where + T: Config, +{ + pub state: PauliSum, + /// Snapshot of the seeded observable, restored by `reset`. + initial: PauliSum, +} + +/// LossyPauliSum-backed executor. Same as `PauliSumExecutor` plus `Loss` / +/// `CorrelatedLoss`; `T` uses `LossyPauliWord` (see `LossyPauliSumConfig`). +#[component] +#[instructions { circuit }] +pub struct LossyPauliSumExecutor +where + T: Config, +{ + pub state: PauliSum, + /// Snapshot of the seeded observable, restored by `reset`. + initial: PauliSum, +} + +macro_rules! paulisum_single_qubit_execute { + ($exec:ident, $leaf:ident, $method:ident) => { + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.state.$method(msg.0); + Ok(()) + } + } + }; +} + +macro_rules! paulisum_two_qubit_execute { + ($exec:ident, $leaf:ident, $method:ident) => { + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack<(usize, usize)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.state.$method(msg.0.0, msg.0.1); + Ok(()) + } + } + }; +} + +macro_rules! paulisum_single_qubit_rotation_execute { + ($exec:ident, $leaf:ident, $method:ident) => { + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack<(usize, f64)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.state.$method(msg.0.0, msg.0.1); + Ok(()) + } + } + }; +} + +macro_rules! paulisum_two_qubit_rotation_execute { + ($exec:ident, $leaf:ident, $method:ident) => { + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack<(usize, usize, f64)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { + self.state.$method(msg.0.0, msg.0.1, msg.0.2); + Ok(()) + } + } + }; +} + +/// Rejects an instruction the backend can't simulate. Message / effect types +/// match `CircuitExecutor` so programs stay backend-agnostic. +macro_rules! paulisum_unsupported_execute { + ($exec:ident, $leaf:ident, $msg:ty, $effect:ty, $why:expr) => { + impl Execute for $exec + where + T: Config, + { + type Effect = $effect; + type Message = $msg; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::$leaf, _: Self::Message) -> eyre::Result<$effect> { + Err(eyre!($why)) + } + } + }; +} + +/// Every `Execute` impl shared by `PauliSumExecutor` and `LossyPauliSumExecutor`. +macro_rules! paulisum_common_execute { + ($exec:ident, $backend:literal) => { + paulisum_single_qubit_execute!($exec, X, x); + paulisum_single_qubit_execute!($exec, Y, y); + paulisum_single_qubit_execute!($exec, Z, z); + paulisum_single_qubit_execute!($exec, H, h); + paulisum_single_qubit_execute!($exec, S, s); + paulisum_single_qubit_execute!($exec, SAdj, s_dag); + paulisum_single_qubit_execute!($exec, SqrtX, sqrt_x); + paulisum_single_qubit_execute!($exec, SqrtXAdj, sqrt_x_dag); + paulisum_single_qubit_execute!($exec, SqrtY, sqrt_y); + paulisum_single_qubit_execute!($exec, SqrtYAdj, sqrt_y_dag); + + paulisum_two_qubit_execute!($exec, CNOT, cnot); + paulisum_two_qubit_execute!($exec, CZ, cz); + + paulisum_single_qubit_rotation_execute!($exec, RX, rx); + paulisum_single_qubit_rotation_execute!($exec, RY, ry); + paulisum_single_qubit_rotation_execute!($exec, RZ, rz); + + paulisum_two_qubit_rotation_execute!($exec, RXX, rxx); + paulisum_two_qubit_rotation_execute!($exec, RYY, ryy); + paulisum_two_qubit_rotation_execute!($exec, RZZ, rzz); + + paulisum_single_qubit_rotation_execute!($exec, Depolarize, depolarize1); + paulisum_two_qubit_rotation_execute!($exec, Depolarize2, depolarize2); + + // Leakage is GeneralizedTableau-only (pinned |0⟩/|1⟩), not a qutrit |L⟩ channel. + paulisum_unsupported_execute!($exec, Measure, PopFromStack, Effects, + concat!("measure is not supported on the ", $backend, " backend")); + paulisum_unsupported_execute!($exec, Reset, PopFromStack, (), + concat!("reset is not supported on the ", $backend, " backend")); + paulisum_unsupported_execute!($exec, Leakage, PopFromStack<(usize, f64, f64)>, (), + concat!("leakage is not supported on the ", $backend, " backend")); + + // TODO: batched gates; need custom messages + + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::T, msg: Self::Message) -> eyre::Result<()> { + self.state.rz(msg.0, std::f64::consts::PI / 8.0); + Ok(()) + } + } + + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::TAdj, msg: Self::Message) -> eyre::Result<()> { + self.state.rz(msg.0, -std::f64::consts::PI / 8.0); + Ok(()) + } + } + + // U3(θ, φ, λ) = RZ(φ)·RY(θ)·RZ(λ); Heisenberg picture, so the tableau's + // rz(λ); ry(θ); rz(φ) runs reversed here. + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack<(usize, f64, f64, f64)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::U3, msg: Self::Message) -> eyre::Result<()> { + let (addr, theta, phi, lam) = msg.0; + self.state.rz(addr, phi); + self.state.ry(addr, theta); + self.state.rz(addr, lam); + Ok(()) + } + } + + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack<(usize, f64, f64)>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::R, msg: Self::Message) -> eyre::Result<()> { + self.state.r(msg.0.0, msg.0.1, msg.0.2); + Ok(()) + } + } + + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = PopFromStack<(usize, [f64; 3])>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::PauliError, msg: Self::Message) -> eyre::Result<()> { + self.state.pauli_error(msg.0.0, msg.0.1); + Ok(()) + } + } + + // CUSTOM MESSAGE: 2 addresses + 15 probabilities exceed vihaco's 16-element tuple limit. + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = vihaco::NoMessage; + type Error = eyre::Report; + + fn execute( + &mut self, + _: &circuit::TwoQubitPauliError, + _: Self::Message, + ) -> eyre::Result<()> { + unimplemented!("TODO: needs a custom message"); + } + } + + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = vihaco::NoMessage; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::Truncate, _: Self::Message) -> eyre::Result<()> { + self.state.truncate(); + Ok(()) + } + } + + // CUSTOM MESSAGE: the Pauli-pattern string can't come off the operand stack. + impl Execute for $exec + where + T: Config, + { + type Effect = (); + type Message = vihaco::NoMessage; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::Trace, _: Self::Message) -> eyre::Result<()> { + unimplemented!("TODO: needs a custom message"); + } + } + }; +} + +paulisum_common_execute!(PauliSumExecutor, "PauliSum"); +paulisum_common_execute!(LossyPauliSumExecutor, "LossyPauliSum"); + +paulisum_unsupported_execute!(PauliSumExecutor, Loss, PopFromStack<(usize, f64)>, (), + "loss is not supported on the PauliSum backend; use the LossyPauliSum backend instead"); +paulisum_unsupported_execute!(PauliSumExecutor, CorrelatedLoss, PopFromStack<(usize, usize, [f64; 3])>, (), + "correlated_loss is not supported on the PauliSum backend; use the LossyPauliSum backend instead"); + +paulisum_single_qubit_rotation_execute!(LossyPauliSumExecutor, Loss, loss_channel); + +impl Execute for LossyPauliSumExecutor +where + T: Config, +{ + type Effect = (); + type Message = PopFromStack<(usize, usize, [f64; 3])>; + type Error = eyre::Report; + + fn execute(&mut self, _: &circuit::CorrelatedLoss, msg: Self::Message) -> eyre::Result<()> { + self.state.correlated_loss_channel(msg.0.0, msg.0.1, msg.0.2); + Ok(()) + } +} + // #[cfg(test)] // mod tests { // use super::*; From d9c92bbaec0d6558a91974dea9ccdb7ca3c38738 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 4 Nov 2026 11:41:33 +0100 Subject: [PATCH 06/36] Start refactoring things to use trait instead of enum --- Cargo.lock | 1 + crates/ppvm-vihaco/Cargo.toml | 1 + crates/ppvm-vihaco/src/component.rs | 627 ++---- crates/ppvm-vihaco/src/composite.rs | 2996 +++++++++++++-------------- crates/ppvm-vihaco/src/lib.rs | 2 +- 5 files changed, 1626 insertions(+), 2001 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63158bdeb..40f52f284 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2408,6 +2408,7 @@ dependencies = [ "smallvec", "vihaco", "vihaco-circuit-isa", + "vihaco-cpu", "vihaco-stdlib", ] diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index f1eb38ab4..c942fcd65 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -27,3 +27,4 @@ vihaco-stdlib = { version = "0.7.0", features = ["program-image-execution"] } vihaco-circuit-isa = { version = "0.1.1", path = "../vihaco-circuit-isa" } ppvm-pauli-sum = { version = "0.1.1", path = "../ppvm-pauli-sum" } rand = "0.10.3" +vihaco-cpu = { version = "0.7.0", features = ["program-image-execution"] } diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index cc1cdc77d..7e585b11d 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -67,33 +67,117 @@ macro_rules! batch_pairs_for { }; } -#[component] -#[instructions { circuit }] -pub struct CircuitExecutor +/// Trait defining all methods required to implement the backend of the VM component +pub trait CircuitBackend { + type Config: Config; + type State: Clifford + + CliffordExtensions + + Reset + + TGate + + U3Gate + + RotationOne + + RotXY + + RotationTwo + + Depolarizing + + Depolarizing2 + + PauliError + + TwoQubitPauliError + + LossChannel + + CorrelatedLossChannel + + LeakageChannel; + + fn state(&mut self) -> &mut Self::State; + + // NOTE: methods that differ between backends get dedicated methods + + fn measure(&mut self, _addr0: usize) -> eyre::Result { + Err(eyre!("measure is not supported on this backend")) + } + + fn leakage(&mut self, _addr0: usize, p0: f64, p1: f61) -> eyre::Result<()> { + Err(eyre!("leakage is not supported on this backend")) + } + + fn loss(&mut self, _addr0: usize, p: f64) -> eyre::Result<()> { + Err(eyre!("loss is not supported on this backend")) + } + + fn correlated_loss(&mut self, _addr0: usize, p0: f64, p1: f64, p2: f64) -> eyre::Result<()> { + Err(eyre!("loss is not supported on this backend")) + } + + fn truncate(&mut self); + + fn reset(&mut self); +} + +impl CircuitBackend for GeneralizedTableau where T: Config, <::Storage as BitView>::Store: PrimInt, I: TableauIndex + Send + Sync + std::fmt::Debug, C: SparseVector + std::fmt::Debug, { - pub tab: GeneralizedTableau, + type Config = T; + type State = GeneralizedTableau; + + fn state(&mut self) -> &mut Self::State { + self + } + + fn measure(&mut self, q: usize) -> eyre::Result { + let m = LossyMeasure::measure(self, q); + let e = match m { + Some(b) => { + if b { + MeasurementOutcome::One + } else { + MeasurementOutcome::Zero + } + } + None => MeasurementOutcome::Lost, + }; + Ok(e) + } + + fn truncate(&mut self) {} + + fn reset(&mut self) { + self.reset_all(); + } +} + +impl CircuitBackend for PauliSum +where + T: Config, +{ + type Config = T; + type State = PauliSum; + + fn state(&mut self) -> &mut Self::State { + self + } + + fn reset(&mut self) { + unimplemented!("TODO") + } +} + +#[component] +#[instructions { circuit }] +pub struct CircuitExecutor { + pub backend: B, } macro_rules! single_qubit_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor - where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, - { + impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.tab.$method(msg.0); + self.backend.state().$method(msg.0); Ok(()) } } @@ -102,19 +186,13 @@ macro_rules! single_qubit_execute { macro_rules! two_qubit_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor - where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, - { + impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, usize)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.tab.$method(msg.0.0, msg.0.1); + self.backend.state().$method(msg.0.0, msg.0.1); Ok(()) } } @@ -123,19 +201,13 @@ macro_rules! two_qubit_execute { macro_rules! single_qubit_rotation_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor - where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, - { + impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.tab.$method(msg.0.0, msg.0.1); + self.backend.state().$method(msg.0.0, msg.0.1); Ok(()) } } @@ -144,19 +216,13 @@ macro_rules! single_qubit_rotation_execute { macro_rules! two_qubit_rotation_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor - where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, - { + impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, usize, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.tab.$method(msg.0.0, msg.0.1, msg.0.2); + self.backend.state().$method(msg.0.0, msg.0.1, msg.0.2); Ok(()) } } @@ -194,121 +260,86 @@ two_qubit_rotation_execute!(Depolarize2, depolarize2); // TODO: batched gates; need custom messages -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, f64, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::U3, msg: Self::Message) -> eyre::Result<()> { - self.tab.u3(msg.0.0, msg.0.1, msg.0.2, msg.0.3); + self.backend.state().u3(msg.0.0, msg.0.1, msg.0.2, msg.0.3); Ok(()) } } -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::R, msg: Self::Message) -> eyre::Result<()> { - self.tab.r(msg.0.0, msg.0.1, msg.0.2); + self.backend.state().r(msg.0.0, msg.0.1, msg.0.2); Ok(()) } } -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::Leakage, msg: Self::Message) -> eyre::Result<()> { - self.tab.leakage_channel(msg.0.0, msg.0.1, msg.0.2); + self.backend + .state() + .leakage_channel(msg.0.0, msg.0.1, msg.0.2); Ok(()) } } -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, [f64; 3])>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::PauliError, msg: Self::Message) -> eyre::Result<()> { - self.tab.pauli_error(msg.0.0, msg.0.1); + self.backend.state().pauli_error(msg.0.0, msg.0.1); Ok(()) } } -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, usize, [f64; 15])>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::TwoQubitPauliError, msg: Self::Message) -> eyre::Result<()> { - self.tab.two_qubit_pauli_error(msg.0.0, msg.0.1, msg.0.2); + // TODO: probably needs a custom message since we can't pop float arrays + self.backend + .state() + .two_qubit_pauli_error(msg.0.0, msg.0.1, msg.0.2); Ok(()) } } -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = PopFromStack<(usize, usize, [f64; 3])>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::CorrelatedLoss, msg: Self::Message) -> eyre::Result<()> { - self.tab.correlated_loss_channel(msg.0.0, msg.0.1, msg.0.2); + self.backend + .state() + .correlated_loss_channel(msg.0.0, msg.0.1, msg.0.2); Ok(()) } } -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = Effects; type Message = PopFromStack; type Error = eyre::Report; fn execute(&mut self, _: &circuit::Measure, msg: Self::Message) -> eyre::Result { - let outcome: MeasurementOutcome = self.tab.measure(msg.0).into(); + let outcome = self.backend.measure(msg.0)?; Ok(Effects::one(CircuitOutcomeEffect::Measurement( MeasurementEffect { measurement_results: smallvec::smallvec![outcome], @@ -317,30 +348,18 @@ where } } -// Truncate is a no-op on the tableau: gate methods already prune via the threshold. -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = vihaco::NoMessage; type Error = eyre::Report; fn execute(&mut self, _: &circuit::Truncate, _: Self::Message) -> eyre::Result<()> { + self.backend.truncate(); Ok(()) } } -impl Execute for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl Execute for CircuitExecutor { type Effect = (); type Message = vihaco::NoMessage; type Error = eyre::Report; @@ -350,398 +369,8 @@ where } } -impl vihaco::Reset for CircuitExecutor -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ +impl vihaco::Reset for CircuitExecutor { fn reset(&mut self) { - self.tab.reset_all(); + self.backend.reset(); } } - -/// PauliSum-backed executor (Heisenberg picture). Answers the same `circuit` -/// dialect as `CircuitExecutor`, but without measurement / reset / loss support. -#[component] -#[instructions { circuit }] -pub struct PauliSumExecutor -where - T: Config, -{ - pub state: PauliSum, - /// Snapshot of the seeded observable, restored by `reset`. - initial: PauliSum, -} - -/// LossyPauliSum-backed executor. Same as `PauliSumExecutor` plus `Loss` / -/// `CorrelatedLoss`; `T` uses `LossyPauliWord` (see `LossyPauliSumConfig`). -#[component] -#[instructions { circuit }] -pub struct LossyPauliSumExecutor -where - T: Config, -{ - pub state: PauliSum, - /// Snapshot of the seeded observable, restored by `reset`. - initial: PauliSum, -} - -macro_rules! paulisum_single_qubit_execute { - ($exec:ident, $leaf:ident, $method:ident) => { - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.state.$method(msg.0); - Ok(()) - } - } - }; -} - -macro_rules! paulisum_two_qubit_execute { - ($exec:ident, $leaf:ident, $method:ident) => { - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack<(usize, usize)>; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.state.$method(msg.0.0, msg.0.1); - Ok(()) - } - } - }; -} - -macro_rules! paulisum_single_qubit_rotation_execute { - ($exec:ident, $leaf:ident, $method:ident) => { - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack<(usize, f64)>; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.state.$method(msg.0.0, msg.0.1); - Ok(()) - } - } - }; -} - -macro_rules! paulisum_two_qubit_rotation_execute { - ($exec:ident, $leaf:ident, $method:ident) => { - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack<(usize, usize, f64)>; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.state.$method(msg.0.0, msg.0.1, msg.0.2); - Ok(()) - } - } - }; -} - -/// Rejects an instruction the backend can't simulate. Message / effect types -/// match `CircuitExecutor` so programs stay backend-agnostic. -macro_rules! paulisum_unsupported_execute { - ($exec:ident, $leaf:ident, $msg:ty, $effect:ty, $why:expr) => { - impl Execute for $exec - where - T: Config, - { - type Effect = $effect; - type Message = $msg; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::$leaf, _: Self::Message) -> eyre::Result<$effect> { - Err(eyre!($why)) - } - } - }; -} - -/// Every `Execute` impl shared by `PauliSumExecutor` and `LossyPauliSumExecutor`. -macro_rules! paulisum_common_execute { - ($exec:ident, $backend:literal) => { - paulisum_single_qubit_execute!($exec, X, x); - paulisum_single_qubit_execute!($exec, Y, y); - paulisum_single_qubit_execute!($exec, Z, z); - paulisum_single_qubit_execute!($exec, H, h); - paulisum_single_qubit_execute!($exec, S, s); - paulisum_single_qubit_execute!($exec, SAdj, s_dag); - paulisum_single_qubit_execute!($exec, SqrtX, sqrt_x); - paulisum_single_qubit_execute!($exec, SqrtXAdj, sqrt_x_dag); - paulisum_single_qubit_execute!($exec, SqrtY, sqrt_y); - paulisum_single_qubit_execute!($exec, SqrtYAdj, sqrt_y_dag); - - paulisum_two_qubit_execute!($exec, CNOT, cnot); - paulisum_two_qubit_execute!($exec, CZ, cz); - - paulisum_single_qubit_rotation_execute!($exec, RX, rx); - paulisum_single_qubit_rotation_execute!($exec, RY, ry); - paulisum_single_qubit_rotation_execute!($exec, RZ, rz); - - paulisum_two_qubit_rotation_execute!($exec, RXX, rxx); - paulisum_two_qubit_rotation_execute!($exec, RYY, ryy); - paulisum_two_qubit_rotation_execute!($exec, RZZ, rzz); - - paulisum_single_qubit_rotation_execute!($exec, Depolarize, depolarize1); - paulisum_two_qubit_rotation_execute!($exec, Depolarize2, depolarize2); - - // Leakage is GeneralizedTableau-only (pinned |0⟩/|1⟩), not a qutrit |L⟩ channel. - paulisum_unsupported_execute!($exec, Measure, PopFromStack, Effects, - concat!("measure is not supported on the ", $backend, " backend")); - paulisum_unsupported_execute!($exec, Reset, PopFromStack, (), - concat!("reset is not supported on the ", $backend, " backend")); - paulisum_unsupported_execute!($exec, Leakage, PopFromStack<(usize, f64, f64)>, (), - concat!("leakage is not supported on the ", $backend, " backend")); - - // TODO: batched gates; need custom messages - - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::T, msg: Self::Message) -> eyre::Result<()> { - self.state.rz(msg.0, std::f64::consts::PI / 8.0); - Ok(()) - } - } - - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::TAdj, msg: Self::Message) -> eyre::Result<()> { - self.state.rz(msg.0, -std::f64::consts::PI / 8.0); - Ok(()) - } - } - - // U3(θ, φ, λ) = RZ(φ)·RY(θ)·RZ(λ); Heisenberg picture, so the tableau's - // rz(λ); ry(θ); rz(φ) runs reversed here. - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack<(usize, f64, f64, f64)>; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::U3, msg: Self::Message) -> eyre::Result<()> { - let (addr, theta, phi, lam) = msg.0; - self.state.rz(addr, phi); - self.state.ry(addr, theta); - self.state.rz(addr, lam); - Ok(()) - } - } - - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack<(usize, f64, f64)>; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::R, msg: Self::Message) -> eyre::Result<()> { - self.state.r(msg.0.0, msg.0.1, msg.0.2); - Ok(()) - } - } - - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = PopFromStack<(usize, [f64; 3])>; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::PauliError, msg: Self::Message) -> eyre::Result<()> { - self.state.pauli_error(msg.0.0, msg.0.1); - Ok(()) - } - } - - // CUSTOM MESSAGE: 2 addresses + 15 probabilities exceed vihaco's 16-element tuple limit. - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = vihaco::NoMessage; - type Error = eyre::Report; - - fn execute( - &mut self, - _: &circuit::TwoQubitPauliError, - _: Self::Message, - ) -> eyre::Result<()> { - unimplemented!("TODO: needs a custom message"); - } - } - - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = vihaco::NoMessage; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::Truncate, _: Self::Message) -> eyre::Result<()> { - self.state.truncate(); - Ok(()) - } - } - - // CUSTOM MESSAGE: the Pauli-pattern string can't come off the operand stack. - impl Execute for $exec - where - T: Config, - { - type Effect = (); - type Message = vihaco::NoMessage; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::Trace, _: Self::Message) -> eyre::Result<()> { - unimplemented!("TODO: needs a custom message"); - } - } - }; -} - -paulisum_common_execute!(PauliSumExecutor, "PauliSum"); -paulisum_common_execute!(LossyPauliSumExecutor, "LossyPauliSum"); - -paulisum_unsupported_execute!(PauliSumExecutor, Loss, PopFromStack<(usize, f64)>, (), - "loss is not supported on the PauliSum backend; use the LossyPauliSum backend instead"); -paulisum_unsupported_execute!(PauliSumExecutor, CorrelatedLoss, PopFromStack<(usize, usize, [f64; 3])>, (), - "correlated_loss is not supported on the PauliSum backend; use the LossyPauliSum backend instead"); - -paulisum_single_qubit_rotation_execute!(LossyPauliSumExecutor, Loss, loss_channel); - -impl Execute for LossyPauliSumExecutor -where - T: Config, -{ - type Effect = (); - type Message = PopFromStack<(usize, usize, [f64; 3])>; - type Error = eyre::Report; - - fn execute(&mut self, _: &circuit::CorrelatedLoss, msg: Self::Message) -> eyre::Result<()> { - self.state.correlated_loss_channel(msg.0.0, msg.0.1, msg.0.2); - Ok(()) - } -} - -// #[cfg(test)] -// mod tests { -// use super::*; - -// fn info(n_qubits: usize) -> PPVMDeviceInfo { -// PPVMDeviceInfo { -// n_qubits, -// ..Default::default() -// } -// } - -// /// Dispatch a single-qubit `Measure` and read the outcome out of the -// /// returned effect. -// fn measure(circuit: &mut Circuit, addr: usize) -> MeasurementOutcome { -// let effects = circuit -// .execute_instruction(&CircuitInstruction::Measure, &CircuitMessage::Qubit(addr)) -// .unwrap(); -// match effects.into_iter().next() { -// Some(CircuitOutcomeEffect::Measurement(m)) => m.measurement_results[0], -// other => panic!("expected a measurement effect, got {other:?}"), -// } -// } - -// /// Smoke test: a single-qubit gate dispatches to the tableau backend and -// /// flips the qubit. -// #[test] -// fn tableau_backend_x_flips_qubit() { -// let mut circuit = Circuit::tableau(&info(1)).unwrap(); -// circuit -// .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) -// .unwrap(); -// assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); -// } - -// /// Smoke test: a two-qubit gate dispatches correctly — `X(0); CNOT(0, 1)` -// /// leaves both qubits in |1⟩. -// #[test] -// fn tableau_backend_cnot_propagates_flip() { -// let mut circuit = Circuit::tableau(&info(2)).unwrap(); -// circuit -// .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) -// .unwrap(); -// circuit -// .execute_instruction(&CircuitInstruction::CNOT, &CircuitMessage::TwoQubit(0, 1)) -// .unwrap(); -// assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); -// assert_eq!(measure(&mut circuit, 1), MeasurementOutcome::One); -// } - -// // ─── Construction rejects more qubits than the backend ceiling ──────── -// // -// // The widest executor bucket is 2048 qubits (U2048 / `[u8; 256]`); beyond -// // that there is no backing width, so the constructors return an error -// // rather than panicking — a panic would tear down the TUI that drives this. - -// #[test] -// fn tableau_rejects_more_than_2048_qubits() { -// assert!(Circuit::tableau(&info(MAX_QUBITS + 1)).is_err()); -// } - -// #[test] -// fn tableau_with_seed_rejects_more_than_2048_qubits() { -// assert!(Circuit::tableau_with_seed(&info(MAX_QUBITS + 1), 0).is_err()); -// } - -// #[test] -// fn paulisum_rejects_more_than_2048_qubits() { -// assert!(Circuit::paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); -// } - -// #[test] -// fn lossy_paulisum_rejects_more_than_2048_qubits() { -// assert!(Circuit::lossy_paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); -// } - -// #[test] -// fn constructs_at_the_2048_qubit_boundary() { -// // 2048 is the last valid bucket; it must still succeed. -// assert!(Circuit::tableau(&info(MAX_QUBITS)).is_ok()); -// } -// } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index e48ec4880..0bd7236d1 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -6,39 +6,34 @@ use vihaco::frame::Frame; use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; use vihaco::traits::{GetProgramInfo, ProgramCounter, StackMemory}; -use vihaco::{Effects, NoContext, Observe, ProgramImage, Type, Value, composite, observe}; -use vihaco_cpu::{CPU, CPUMessage}; - -/// Re-exported so consumers (e.g. the CLI debugger) can match on step results -/// without depending on `vihaco-cpu` directly. -pub use vihaco_cpu::StepOutcome; - -use crate::component::Circuit; -#[cfg(test)] -use crate::component::TableauCircuit; -use crate::measurements::{ - CircuitOutcomeEffect, MeasurementEffect, MeasurementObserver, MeasurementResult, TraceEffect, - TraceObserver, +use vihaco::{ + Effects, NoContext, Observe, ProgramImage, Step, Type, Value, composite, observe, program, }; -use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; +use vihaco_cpu::CPU; + +use crate::component::{CircuitBackend, CircuitExecutor}; +use crate::measurements::{MeasurementObserver, TraceObserver}; + +use vihaco_circuit_isa::circuit; // Device configuration lives in `device_info` (shipped with the circuit // component so it compiles without the composite). Re-exported here so the // existing `crate::composite::{…}` paths keep resolving. pub use crate::device_info::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo}; -pub type PPVMInstruction = ppvm::runtime::Instruction; - +// #[derive(Default)] #[composite] -#[derive(Default)] -pub struct PPVM { - loader: ProgramImage, +pub struct PPVM { + #[device(0)] + #[program] + execution: vihaco_stdlib::ExecutionState, - #[device(0x00)] - cpu: CPU, + #[device(1)] + cpu: vihaco_cpu::CPU, - #[device(0x01)] - circuit: Circuit, + // NOTE: we need the full import path here for some vihaco macro magic reasons + #[device(2)] + circuit: crate::component::CircuitExecutor, stdout: StdoutObserver, @@ -47,1481 +42,1480 @@ pub struct PPVM { trace_record: TraceObserver, } -pub(crate) use self::ppvm as ppvm_module; - -#[derive(Debug, Clone)] -pub enum PPVMEffect { - Step(StepOutcome), - Stdout(StdoutEffect), - Circuit(Box), - Measurement(MeasurementEffect), - Trace(TraceEffect), -} - -#[observe(vihaco::observer::stdio::StdoutEffect, effect = PPVMEffect)] -impl PPVM { - fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> eyre::Result> { - Observe::::observe(&mut self.stdout, effect) - } -} - -#[observe(CircuitEffect, effect = PPVMEffect)] -impl PPVM { - fn observe_circuit_effect( - &mut self, - effect: &CircuitEffect, - ) -> eyre::Result> { - Observe::::observe(&mut self.circuit, effect) - } -} - -#[observe(MeasurementEffect, effect = PPVMEffect)] -impl PPVM { - fn observe_measurement_effect( - &mut self, - effect: &MeasurementEffect, - ) -> eyre::Result> { - Observe::::observe(&mut self.measurement_record, effect) - } -} - -#[observe(TraceEffect, effect = PPVMEffect)] -impl PPVM { - fn observe_trace_effect(&mut self, effect: &TraceEffect) -> eyre::Result> { - Observe::::observe(&mut self.trace_record, effect) - } -} - -impl From for PPVMEffect { - fn from(value: StdoutEffect) -> Self { - Self::Stdout(value) - } -} - -impl From for PPVMEffect { - fn from(value: MeasurementEffect) -> Self { - Self::Measurement(value) - } -} - -impl From for PPVMEffect { - fn from(value: TraceEffect) -> Self { - Self::Trace(value) - } -} - -impl From for PPVMEffect { - fn from(value: CircuitOutcomeEffect) -> Self { - match value { - CircuitOutcomeEffect::Measurement(m) => Self::Measurement(m), - CircuitOutcomeEffect::Trace(t) => Self::Trace(t), - } - } -} - -impl std::fmt::Display for PPVMInstruction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PPVMInstruction::Cpu(inst) => inst.fmt(f), - PPVMInstruction::Circuit(inst) => inst.fmt(f), - } - } -} - -impl PartialEq for PPVMInstruction { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (PPVMInstruction::Cpu(a), PPVMInstruction::Cpu(b)) => a == b, - (PPVMInstruction::Circuit(a), PPVMInstruction::Circuit(b)) => a == b, - _ => false, - } - } -} - -impl From for PPVMInstruction { - fn from(value: vihaco_cpu::RuntimeInstruction) -> Self { - Self::Cpu(value) - } -} - -impl From for PPVMInstruction { - fn from(value: CircuitInstruction) -> Self { - Self::Circuit(value) - } -} - -impl PPVM { - fn resolve_cpu(&mut self, inst: &vihaco_cpu::RuntimeInstruction) -> eyre::Result { - match inst { - vihaco_cpu::RuntimeInstruction::IndirectCall => { - let function_id: u32 = self.cpu.stack_top()?.get_function_ref()?; - let function = self.loader.get_function(function_id as usize)?; - Ok(CPUMessage::FunctionInfo { - arity: function.signature.params.len() as u32, - start_address: function.start_address, - }) - } - vihaco_cpu::RuntimeInstruction::Print => { - let value = *self.cpu.stack_top()?; - match value { - vihaco::Value::String(addr) => { - let string = self.loader.get_string(addr as usize)?.clone(); - Ok(CPUMessage::Print(string)) - } - value => Ok(CPUMessage::Print(value.to_string())), - } - } - _ => Ok(CPUMessage::None), - } - } - - fn resolve_circuit(&mut self, inst: &CircuitInstruction) -> eyre::Result { - use CircuitInstruction::*; - match inst { - X | Y | Z | H | S | SAdj | SqrtX | SqrtY | SqrtXAdj | SqrtYAdj | T | TAdj | Measure - | Reset => { - let q = self.pop_u64()?; - Ok(CircuitMessage::Qubit(q)) - } - CNOT | CZ => { - let q1 = self.pop_u64()?; - let q0 = self.pop_u64()?; - Ok(CircuitMessage::TwoQubit(q0, q1)) - } - RX | RY | RZ | Depolarize | Loss => { - let theta = self.pop_f64()?; - let q = self.pop_u64()?; - Ok(CircuitMessage::QubitAndFloat(q, theta)) - } - Leakage => { - // Push order: qubit, p0, p1. Pop reverse. - let p1 = self.pop_f64()?; - let p0 = self.pop_f64()?; - let q = self.pop_u64()?; - Ok(CircuitMessage::QubitAndTwoFloats(q, p0, p1)) - } - RXX | RYY | RZZ | Depolarize2 => { - let theta = self.pop_f64()?; - let q1 = self.pop_u64()?; - let q0 = self.pop_u64()?; - Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) - } - R => { - let theta = self.pop_f64()?; - let axis_angle = self.pop_f64()?; - let q = self.pop_u64()?; - Ok(CircuitMessage::QubitAndTwoFloats(q, axis_angle, theta)) - } - U3 => { - let lam = self.pop_f64()?; - let phi = self.pop_f64()?; - let theta = self.pop_f64()?; - let q = self.pop_u64()?; - Ok(CircuitMessage::QubitU3(q, theta, phi, lam)) - } - - // TODO: pop actual float arrays? - PauliError => { - let pz = self.pop_f64()?; - let py = self.pop_f64()?; - let px = self.pop_f64()?; - let q = self.pop_u64()?; - Ok(CircuitMessage::QubitAndFloatArr3(q, [px, py, pz])) - } - CorrelatedLoss => { - let p2 = self.pop_f64()?; - let p1 = self.pop_f64()?; - let p0 = self.pop_f64()?; - let q1 = self.pop_u64()?; - let q0 = self.pop_u64()?; - Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) - } - TwoQubitPauliError => { - let mut ps = [0.0; 15]; - for p in ps.iter_mut().rev() { - *p = self.pop_f64()?; - } - let q1 = self.pop_u64()?; - let q0 = self.pop_u64()?; - Ok(CircuitMessage::TwoQubitAndFloatArr15(q0, q1, ps)) - } - Trace => { - let s = self.pop_string()?; - Ok(CircuitMessage::PauliPatternStr(s)) - } - Truncate => Ok(CircuitMessage::None), - } - } - - fn pop_u64(&mut self) -> eyre::Result { - match self.cpu.stack_pop()? { - vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into), - v => Err(eyre::eyre!("Expected u64, got {:?}", v)), - } - } - - fn pop_f64(&mut self) -> eyre::Result { - match self.cpu.stack_pop()? { - vihaco::Value::F64(v) => Ok(v), - v => Err(eyre::eyre!("Expected f64 argument, got {:?}", v)), - } - } - - /// Pop a `Value::String(addr)`, look up the addr in the module's string - /// table, and return the owned string. Used by `Trace` to resolve its - /// Pauli-pattern operand before the executor sees the message. - fn pop_string(&mut self) -> eyre::Result { - match self.cpu.stack_pop()? { - vihaco::Value::String(addr) => Ok(self.loader.get_string(addr as usize)?.clone()), - v => Err(eyre::eyre!("Expected string operand, got {:?}", v)), - } - } - - pub fn init(&mut self) -> eyre::Result<()> { - self.init_inner(None) - } - - /// Like [`PPVM::init`], but seed the circuit RNG deterministically so the - /// run is reproducible. - pub fn init_with_seed(&mut self, seed: u64) -> eyre::Result<()> { - self.init_inner(Some(seed)) - } - - fn init_inner(&mut self, seed: Option) -> eyre::Result<()> { - let info = &self.loader.module.extra; - if info.n_qubits == 0 { - return Err(eyre::eyre!("device circuit.n_qubits must be declared")); - } - self.circuit = match (info.backend, seed) { - (BackendKind::Tableau, None) => Circuit::tableau(info)?, - (BackendKind::Tableau, Some(seed)) => Circuit::tableau_with_seed(info, seed)?, - (BackendKind::PauliSum, _) => { - let terms = parse_observable_terms(info)?; - Circuit::paulisum(info, &terms)? - } - (BackendKind::LossyPauliSum, _) => { - let terms = parse_observable_terms(info)?; - Circuit::lossy_paulisum(info, &terms)? - } - }; - - // push entry frame - self.cpu.push_frame(Frame { - base: 0, - span: (0, 0, 0), - function: None, - ret_pc: 0, - }); - - Ok(()) - } - - pub fn load( - &mut self, - module: &vihaco::module::LocalModule< - PPVMInstruction, - vihaco::Value, - vihaco::Type, - PPVMDeviceInfo, - >, - ) -> eyre::Result<()> { - self.loader.module = module.clone(); - Ok(()) - } - - pub fn step_once(&mut self) -> eyre::Result { - let inst = self.loader.peek_instruction()?.clone(); - let effects = self.execute_effects(inst)?; - self.continue_effects(effects) - } - - /// Program counter: the index of the next instruction to execute. - pub fn current_pc(&self) -> u32 { - self.loader.pc() - } - - /// The next instruction to execute, or `None` once execution has run off - /// the end of the code. Intended for debuggers/inspection. - pub fn current_instruction(&self) -> Option { - self.loader.peek_instruction().ok().cloned() - } - - /// Append one REPL command's lowered VM ops and run just that block against - /// the persistent state, advancing the pc through it. NOTE: an out-of-range - /// qubit index *panics* in the tableau rather than erroring, so callers must - /// bounds-check qubit operands against `n_qubits` first. - pub fn execute_single_instruction(&mut self, instrs: &[PPVMInstruction]) -> eyre::Result<()> { - let start = self.loader.module.code.len() as u32; - self.loader.module.code.extend_from_slice(instrs); - *self.loader.pc_mut() = start; - for _ in 0..instrs.len() { - self.step_once()?; - } - Ok(()) - } - - /// Render the current circuit state (tableau / Pauli sum) for the REPL's - /// `show` command. Delegates to the circuit's size-specific tableau. - pub fn state_string(&self) -> String { - self.circuit.state_string() - } - - /// Build a fresh, initialized `n_qubits`-qubit device with no code. The - /// REPL's `device` command uses this to (re)create the machine. Errors if - /// `n_qubits` is zero (a device must have at least one qubit). - pub fn with_qubits(n_qubits: usize) -> eyre::Result { - let mut machine = Self::default(); - let mut module = vihaco::module::LocalModule::< - PPVMInstruction, - Value, - vihaco::Type, - PPVMDeviceInfo, - >::default(); - module.extra.n_qubits = n_qubits; - machine.load(&module)?; - machine.init()?; - Ok(machine) - } - - /// Lower a single circuit instruction — qubit operands first, then float - /// params, per the push-in-order / pop-in-reverse convention — and execute - /// it against the persistent state. Qubit indices are bounds-checked against - /// the device size first, because the tableau panics (rather than erroring) - /// on an out-of-range qubit. - pub fn apply_circuit_instruction( - &mut self, - inst: CircuitInstruction, - qubits: &[usize], - params: &[f64], - ) -> eyre::Result<()> { - let n_qubits = self.loader.module.extra.n_qubits; - for &q in qubits { - if q >= n_qubits { - eyre::bail!("qubit {q} out of range for {n_qubits}-qubit device"); - } - } - - let mut instrs = Vec::with_capacity(qubits.len() + params.len() + 1); - for &q in qubits { - instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(q as u64), - ))); - } - for &p in params { - instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::F64, - Value::F64(p), - ))); - } - instrs.push(PPVMInstruction::Circuit(inst)); - - // Run the appended block, then roll back pc, code, and operand stack (on - // every path, including error) so the injected op is transparent to a - // paused program — only its tableau/measurement effects persist. - let saved_pc = self.loader.pc(); - let saved_len = self.loader.module.code.len(); - let saved_stack = self.cpu.stack_len(); - let result = self.execute_single_instruction(&instrs); - self.loader.module.code.truncate(saved_len); - *self.loader.pc_mut() = saved_pc; - self.cpu.stack_mut().truncate(saved_stack); - result - } - - fn execute_effects(&mut self, inst: PPVMInstruction) -> eyre::Result> { - log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); - match inst { - PPVMInstruction::Cpu(cpu_inst) => { - let msg = self.resolve_cpu(&cpu_inst)?; - self.cpu.set_current_pc(self.loader.pc()); - let stdout_effect = match (&cpu_inst, &msg) { - ( - vihaco_cpu::RuntimeInstruction::Print, - vihaco_cpu::CPUMessage::Print(text), - ) => Some(PPVMEffect::Stdout(StdoutEffect(text.clone()))), - _ => None, - }; - let outcome = vihaco::expect_exactly_one_effect( - vihaco::GeneratedComponent::execute_generated(&mut self.cpu, cpu_inst, msg)?, - )?; - // Advance past a cpu::cpu.breakpoint as well, so the debugger that paused - // on it doesn't re-hit the same instruction on the next step. - if matches!(outcome, StepOutcome::Continue | StepOutcome::Breakpoint) { - if let Some(target) = self.cpu.take_pending_pc() { - *self.loader.pc_mut() = target; - } else { - *self.loader.pc_mut() += 1; - } - } - let mut effects = Effects::one(PPVMEffect::Step(outcome)); - if let Some(stdout_effect) = stdout_effect { - effects = effects.append(stdout_effect); - } - Ok(effects) - } - PPVMInstruction::Circuit(inst) => { - let msg = self.resolve_circuit(&inst)?; - let circuit_effects = ::execute_generated( - &mut self.circuit, - inst, - msg, - )?; - *self.loader.pc_mut() += 1; - let mut effects = Effects::one(PPVMEffect::Step(StepOutcome::Continue)); - for outcome in circuit_effects { - effects = effects.append(PPVMEffect::from(outcome)); - } - Ok(effects) - } - } - } - - fn continue_effects(&mut self, effects: Effects) -> eyre::Result { - let mut step_outcome = None; - for effect in effects { - match effect { - PPVMEffect::Step(outcome) => { - if step_outcome.replace(outcome).is_some() { - return Err(eyre::eyre!( - "expected exactly one PPVM step effect, got multiple" - )); - } - } - effect => self.continue_observer_effect(effect)?, - } - } - - step_outcome.ok_or_else(|| eyre::eyre!("expected exactly one PPVM step effect, got 0")) - } - - fn continue_observer_effect(&mut self, effect: PPVMEffect) -> eyre::Result<()> { - match effect { - PPVMEffect::Stdout(effect) => { - let follow_ups = Observe::::observe(self, &effect)?; - self.continue_observer_effects(follow_ups) - } - PPVMEffect::Circuit(effect) => { - let follow_ups = Observe::::observe(self, &effect)?; - self.continue_observer_effects(follow_ups) - } - PPVMEffect::Measurement(effect) => { - let follow_ups = Observe::::observe(self, &effect)?; - // NOTE: push measurement outcomes onto the stack as u32 (0/1/2, where 2 = Lost). - for outcome in effect.measurement_results { - let m = Value::U32(outcome as u32); - self.cpu.stack_push(m); - } - self.continue_observer_effects(follow_ups) - } - PPVMEffect::Trace(effect) => { - let value = effect.value; - let follow_ups = Observe::::observe(self, &effect)?; - // Mirror the measurement wiring: append to the trace record - // (via the observer above) AND push the value onto the CPU - // stack so user bytecode can consume it. Plan Task 7. - self.cpu.stack_push(Value::F64(value)); - self.continue_observer_effects(follow_ups) - } - PPVMEffect::Step(_) => Err(eyre::eyre!( - "unexpected Step effect while continuing PPVM observer follow-ups" - )), - } - } - - fn continue_observer_effects(&mut self, effects: Effects) -> eyre::Result<()> { - for effect in effects { - self.continue_observer_effect(effect)?; - } - Ok(()) - } - - pub fn run(&mut self) -> eyre::Result { - self.run_with_seed(None) - } - - /// Like [`PPVM::run`], but seed the circuit RNG deterministically when - /// `seed` is `Some`, making the run reproducible. - pub fn run_with_seed(&mut self, seed: Option) -> eyre::Result { - match seed { - Some(seed) => self.init_with_seed(seed)?, - None => self.init()?, - } - - loop { - // Breakpoints only pause the interactive debugger; a batch run - // skips straight past them. - match self.step_once()? { - StepOutcome::Continue | StepOutcome::Breakpoint => continue, - action => return Ok(action), - } - } - } - - pub fn stdout(&self) -> &[u8] { - self.stdout.output() - } - - pub fn measurement_record(&self) -> Vec { - self.measurement_record.record.clone() - } - - /// Per-trace values collected by `Trace` instructions during the run. - /// Parallel to [`PPVM::measurement_record`]: one f64 per `Trace` executed, - /// in execution order. - pub fn trace_record(&self) -> Vec { - self.trace_record.record.clone() - } - - pub fn load_program(&mut self, program: &str) -> eyre::Result<()> { - let module = crate::compile_program(program)?; - self.load(&module)?; - Ok(()) - } - - /// Load from a file, auto-detecting the format: if it starts with the PPVM - /// magic it is loaded as `.ssb` bytecode, otherwise it is parsed as `.sst` - /// source text. A magic match commits to the bytecode path — a corrupt - /// `.ssb` errors rather than silently falling back to the text parser. - pub fn load_file(&mut self, path: &str) -> eyre::Result<()> { - let bytes = std::fs::read(path)?; - if crate::bytecode::is_bytecode(&bytes) { - self.load_bytecode(&bytes) - } else { - self.load_program(std::str::from_utf8(&bytes)?) - } - } - - /// Load a module from an in-memory `.ssb` byte stream. - pub fn load_bytecode(&mut self, bytes: &[u8]) -> eyre::Result<()> { - let module = crate::bytecode::module_from_bytes(bytes)?; - self.load(&module) - } - - /// Read a `.ssb` file and load the module it contains. - pub fn load_bytecode_file(&mut self, path: &str) -> eyre::Result<()> { - let bytes = std::fs::read(path)?; - self.load_bytecode(&bytes) - } - - pub fn run_program(&mut self, program: &str) -> eyre::Result<()> { - self.load_program(program)?; - self.run()?; - Ok(()) - } - - pub fn run_file(&mut self, path: &str) -> eyre::Result<()> { - self.load_file(path)?; - self.run()?; - Ok(()) - } -} - -impl vihaco::Reset for PPVM { - fn reset(&mut self) { - self.cpu.reset(); - self.circuit.reset(); - self.loader.pc = 0; - self.measurement_record.record.clear(); - self.trace_record.record.clear(); - } -} - -/// Parse the `device circuit.observable` header into Pauli-sum terms ready to -/// seed a `PauliSum` / `LossyPauliSum` state. Single-Pauli observables from -/// Phase 2 keep working as the degenerate one-term case; multi-term sums like -/// `"1.0*ZZ + 0.5*XX"` are handled by [`parse_pauli_sum_terms`]. -fn parse_observable_terms(info: &PPVMDeviceInfo) -> Result> { - let observable = info.observable.as_deref().ok_or_else(|| { - eyre!( - "the {:?} backend requires `device circuit.observable` to be set", - info.backend - ) - })?; - crate::observable::parse_pauli_sum_terms(observable, info.n_qubits) -} - -#[cfg(test)] -mod tests { - use vihaco::{Type, Value, module::LocalModule}; - - use super::*; - - #[test] - fn test_run_ppvm() -> eyre::Result<()> { - let mut module: LocalModule = - LocalModule::default(); - - module.extra.n_qubits = 2; - - /* - cpu::cpu.const u64, 0 - circuit::circuit.h - */ - let zero = PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(0), - )); - let one = PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(1), - )); - module.code.push(zero.clone()); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::H)); - - /* - cpu::cpu.const u64, 0 - circuit::circuit.t - */ - - module.code.push(zero.clone()); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::T)); - - /* - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot - */ - module.code.push(zero.clone()); - module.code.push(one.clone()); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::CNOT)); - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - - for _ in 0..module.code.len() { - machine.step_once()?; - assert!(machine.cpu.stack().len() <= 2); - } - - let num_coefficients = match &machine.circuit { - Circuit::Tableau(TableauCircuit::Bits64(ex)) => { - println!("{}", ex.tab); - ex.tab.coefficients.len() - } - Circuit::Tableau(TableauCircuit::Bits128(ex)) => { - println!("{}", ex.tab); - ex.tab.coefficients.len() - } - Circuit::Tableau(TableauCircuit::Bits256(ex)) => { - println!("{}", ex.tab); - ex.tab.coefficients.len() - } - Circuit::Tableau(TableauCircuit::Bits512(ex)) => { - println!("{}", ex.tab); - ex.tab.coefficients.len() - } - Circuit::Tableau(TableauCircuit::Bits1024(ex)) => { - println!("{}", ex.tab); - ex.tab.coefficients.len() - } - Circuit::Tableau(TableauCircuit::Bits2048(ex)) => { - println!("{}", ex.tab); - ex.tab.coefficients.len() - } - Circuit::PauliSum(_) | Circuit::LossyPauliSum(_) => { - panic!("test expects the default Tableau backend, got a PauliSum variant"); - } - }; - - assert_eq!(num_coefficients, 2); - Ok(()) - } - - #[test] - fn test_device_decl() -> eyre::Result<()> { - // Equivalent .sst source: - // - // device circuit.n_qubits 5; - // device circuit.coefficient_threshold 1e-10; - // - // fn @main() { ...5-qubit GHZ + 5 measurements... } - - let mut module: LocalModule = - LocalModule::default(); - - module.extra.n_qubits = 5; - module.extra.coefficient_threshold = 1e-10; - - // 5-qubit GHZ: H on q0, then CNOT(q_i, q_{i+1}) for i = 0..4. - /* - cpu::cpu.const u64, 0 - circuit::circuit.h - */ - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(0), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::H)); - - for i in 0..4u64 { - /* - cpu::cpu.const u64, i - cpu::cpu.const u64, i+1 - circuit::circuit.cnot - */ - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(i), - ))); - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(i + 1), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::CNOT)); - } - - // Measure all 5 qubits. - for q in 0..5u64 { - /* - cpu::cpu.const u64, q - circuit::circuit.measure - */ - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(q), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::Measure)); - } - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - - for _ in 0..module.code.len() { - machine.step_once()?; - } - - assert_eq!(machine.measurement_record().len(), 5); - Ok(()) - } - - // ─── Incremental execution (REPL) ───────────────────────────────────── - - #[test] - fn execute_single_instruction_persists_state_across_calls() -> eyre::Result<()> { - use crate::measurements::MeasurementOutcome; - - // A 1-qubit device with no code; the REPL builds up instructions - // incrementally, one command at a time, rather than loading a program. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - - // First command: X on q0 (|0> -> |1>). - let x = [ - PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(0), - )), - PPVMInstruction::Circuit(CircuitInstruction::X), - ]; - machine.execute_single_instruction(&x)?; - // No measurement yet. - assert!(machine.measurement_record().is_empty()); - - // Second command: measure q0. The X from the first command must persist, - // so the outcome is deterministically |1>. - let measure = [ - PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(0), - )), - PPVMInstruction::Circuit(CircuitInstruction::Measure), - ]; - machine.execute_single_instruction(&measure)?; - - let record = machine.measurement_record(); - assert_eq!(record.len(), 1); - assert_eq!(record[0].as_slice(), [MeasurementOutcome::One]); - Ok(()) - } - - #[test] - fn execute_single_instruction_propagates_engine_errors() -> eyre::Result<()> { - // The REPL relies on engine errors surfacing as `Err` (so it can print - // them and keep looping) rather than panicking. A circuit with no qubit - // operand on the stack is one such propagating error. - // - // NOTE: an out-of-range qubit index (>= n_qubits) currently *panics* in - // the tableau rather than erroring, so the REPL command layer must - // bounds-check qubit indices before calling `execute`. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - - // `circuit::circuit.h` with nothing on the stack: `pop_u64` fails. - let missing_operand = [PPVMInstruction::Circuit(CircuitInstruction::H)]; - assert!( - machine - .execute_single_instruction(&missing_operand) - .is_err() - ); - Ok(()) - } - - #[test] - fn state_string_renders_a_small_device() -> eyre::Result<()> { - let source = "device circuit.n_qubits 2;\nfn @main() { cpu::cpu.ret 0 }\n"; - let mut machine = PPVM::default(); - machine.load_program(source)?; - machine.init()?; - - let rendered = machine.state_string(); - assert!( - !rendered.is_empty(), - "state_string should render the tableau" - ); - // PPVM delegates to the circuit. - assert_eq!(rendered, machine.circuit.state_string()); - Ok(()) - } - - #[test] - fn resolve_circuit_pops_operands_in_reverse_of_push_order() -> eyre::Result<()> { - // Convention: operands are pushed in argument order (q0, q1, then any - // floats) and popped in reverse. So every two-qubit circuit must read q0 as - // the first operand pushed, consistently, with or without trailing - // floats. (CNOT already obeyed this; the float-carrying arms did not.) - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 8; - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - - // CNOT: push q0=2, q1=5. - machine.cpu.stack_push(Value::U64(2)); - machine.cpu.stack_push(Value::U64(5)); - assert_eq!( - machine.resolve_circuit(&CircuitInstruction::CNOT)?, - CircuitMessage::TwoQubit(2, 5) - ); - - // RXX: push q0=2, q1=5, theta — same qubit order as CNOT. - machine.cpu.stack_push(Value::U64(2)); - machine.cpu.stack_push(Value::U64(5)); - machine.cpu.stack_push(Value::F64(0.3)); - assert_eq!( - machine.resolve_circuit(&CircuitInstruction::RXX)?, - CircuitMessage::TwoQubitAndFloat(2, 5, 0.3) - ); - - // CorrelatedLoss: push q0=2, q1=5, p0, p1, p2. - machine.cpu.stack_push(Value::U64(2)); - machine.cpu.stack_push(Value::U64(5)); - machine.cpu.stack_push(Value::F64(0.1)); - machine.cpu.stack_push(Value::F64(0.2)); - machine.cpu.stack_push(Value::F64(0.3)); - assert_eq!( - machine.resolve_circuit(&CircuitInstruction::CorrelatedLoss)?, - CircuitMessage::TwoQubitAndFloatArr3(2, 5, [0.1, 0.2, 0.3]) - ); - - // Leakage: push q=2, p0, p1 — two floats, unlike Loss. - machine.cpu.stack_push(Value::U64(2)); - machine.cpu.stack_push(Value::F64(0.1)); - machine.cpu.stack_push(Value::F64(0.2)); - assert_eq!( - machine.resolve_circuit(&CircuitInstruction::Leakage)?, - CircuitMessage::QubitAndTwoFloats(2, 0.1, 0.2) - ); - Ok(()) - } - - #[test] - fn with_qubits_builds_an_initialized_device() -> eyre::Result<()> { - use crate::measurements::MeasurementOutcome; - - let mut m = PPVM::with_qubits(2)?; - // The device is ready to take instructions immediately. - m.apply_circuit_instruction(CircuitInstruction::X, &[0], &[])?; - m.apply_circuit_instruction(CircuitInstruction::Measure, &[0], &[])?; - let record = m.measurement_record(); - assert_eq!(record.len(), 1); - assert_eq!(record[0].as_slice(), [MeasurementOutcome::One]); - Ok(()) - } - - #[test] - fn with_qubits_zero_is_an_error() { - assert!(PPVM::with_qubits(0).is_err()); - } - - #[test] - fn apply_circuit_instruction_bounds_checks_qubits() -> eyre::Result<()> { - // q1 is out of range on a 1-qubit device: this must error rather than - // panic in the tableau. - let mut m = PPVM::with_qubits(1)?; - let err = m - .apply_circuit_instruction(CircuitInstruction::X, &[1], &[]) - .unwrap_err(); - assert!(err.to_string().contains("out of range"), "got: {err}"); - Ok(()) - } - - // ─── Parser-driven entry points ─────────────────────────────────────── - - #[test] - fn load_program_populates_device_info_and_code() -> eyre::Result<()> { - let source = "device circuit.n_qubits 2;\n\ - device circuit.coefficient_threshold 1e-8;\n\ - fn @main() {\n\ - cpu::cpu.const u64, 0\n\ - circuit::circuit.h\n\ - cpu::cpu.ret 0\n\ - }\n"; - let mut machine = PPVM::default(); - machine.load_program(source)?; - assert_eq!(machine.loader.module.extra.n_qubits, 2); - assert_eq!(machine.loader.module.extra.coefficient_threshold, 1e-8); - // cpu::cpu.const u64, 0 / circuit::circuit.h / ret = 3 - assert_eq!(machine.loader.module.code.len(), 3); - Ok(()) - } - - #[test] - fn run_program_executes_bell_circuit() -> eyre::Result<()> { - let source = "device circuit.n_qubits 2;\n\ - fn @main() {\n\ - cpu::cpu.const u64, 0\n\ - circuit::circuit.h\n\ - cpu::cpu.const u64, 0\n\ - cpu::cpu.const u64, 1\n\ - circuit::circuit.cnot\n\ - cpu::cpu.const u64, 0\n\ - circuit::circuit.measure\n\ - cpu::cpu.const u64, 1\n\ - circuit::circuit.measure\n\ - cpu::cpu.ret 0\n\ - }\n"; - let mut machine = PPVM::default(); - machine.run_program(source)?; - let record = machine.measurement_record(); - assert_eq!(record.len(), 2); - Ok(()) - } - - #[test] - fn reset_clears_the_measurement_record() -> eyre::Result<()> { - use vihaco::Reset; - - let source = "device circuit.n_qubits 2;\n\ - fn @main() {\n\ - cpu::cpu.const u64, 0\n\ - circuit::circuit.h\n\ - cpu::cpu.const u64, 0\n\ - cpu::cpu.const u64, 1\n\ - circuit::circuit.cnot\n\ - cpu::cpu.const u64, 0\n\ - circuit::circuit.measure\n\ - cpu::cpu.const u64, 1\n\ - circuit::circuit.measure\n\ - cpu::cpu.ret 0\n\ - }\n"; - let mut machine = PPVM::default(); - machine.run_program(source)?; - assert_eq!(machine.measurement_record().len(), 2); - - // Resetting the machine must discard the recorded measurements, so a - // subsequent run does not see stale results leaking in from before. - machine.reset(); - assert!( - machine.measurement_record().is_empty(), - "reset must clear the measurement record" - ); - - Ok(()) - } - - #[test] - fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { - let source = "fn @main() { cpu::cpu.ret 0 }\n"; - let mut machine = PPVM::default(); - machine.load_program(source)?; - let err = machine.init().unwrap_err(); - assert!(err.to_string().contains("circuit.n_qubits"), "err: {err}"); - Ok(()) - } - - #[test] - fn run_program_reports_parse_errors() { - let source = "device circuit.n_qubits 2;\n\ - fn @main() {\n\ - circuit::circuit.not_a_real_gate\n\ - cpu::cpu.ret 0\n\ - }\n"; - let mut machine = PPVM::default(); - let err = machine.run_program(source).unwrap_err(); - assert!( - err.to_string().contains("parsing failed") - || err.to_string().contains("parsing functions failed") - || err.to_string().contains("unhandled raw form"), - "err: {err}" - ); - } - - // ─── Breakpoints ────────────────────────────────────────────────────── - - /// Bell circuit with a `cpu::cpu.breakpoint` between the two measurements. - const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 2;\n\ - fn @main() {\n\ - cpu::cpu.const u64, 0\n\ - circuit::circuit.h\n\ - cpu::cpu.const u64, 0\n\ - cpu::cpu.const u64, 1\n\ - circuit::circuit.cnot\n\ - cpu::cpu.const u64, 0\n\ - circuit::circuit.measure\n\ - cpu::cpu.breakpoint\n\ - cpu::cpu.const u64, 1\n\ - circuit::circuit.measure\n\ - cpu::cpu.ret 0\n\ - }\n"; - - #[test] - fn run_ignores_breakpoints() -> eyre::Result<()> { - // A batch run must execute straight through the cpu::cpu.breakpoint and record - // both measurements, exactly as if it weren't there. - let mut machine = PPVM::default(); - machine.run_program(BREAKPOINT_PROGRAM)?; - assert_eq!(machine.measurement_record().len(), 2); - Ok(()) - } - - #[test] - fn step_once_advances_past_breakpoint() -> eyre::Result<()> { - let mut machine = PPVM::default(); - machine.load_program(BREAKPOINT_PROGRAM)?; - machine.init()?; - - // Step until the cpu::cpu.breakpoint pauses us. - let mut outcome = StepOutcome::Continue; - for _ in 0..machine.loader.module.code.len() { - outcome = machine.step_once()?; - if outcome == StepOutcome::Breakpoint { - break; - } - } - assert_eq!( - outcome, - StepOutcome::Breakpoint, - "cpu::cpu.breakpoint should pause" - ); - let pc_at_break = machine.current_pc(); - - // Stepping again must make progress (advance the pc) rather than - // re-hitting the same cpu::cpu.breakpoint instruction. - let next = machine.step_once()?; - assert_ne!( - next, - StepOutcome::Breakpoint, - "must move past the cpu::cpu.breakpoint" - ); - assert!(machine.current_pc() > pc_at_break, "pc must advance"); - - Ok(()) - } - - #[test] - fn paulisum_truncate_runs_without_error() -> eyre::Result<()> { - // Smoke test: a `circuit::circuit.truncate` reaches the PauliSum executor's - // Truncate arm and calls `state.truncate()`. Task 8 makes the - // observable mandatory for PauliSum init, so seed `Z` here. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - module.extra.backend = BackendKind::PauliSum; - module.extra.observable = Some("Z".to_string()); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::Truncate)); - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - machine.step_once()?; - Ok(()) - } - - #[test] - fn paulisum_trace_populates_trace_record() -> eyre::Result<()> { - // End-to-end Trace pipeline: with the observable `Z` seeded (Task 8), - // tracing the `Z0` pattern picks up that one term with coefficient - // 1.0, so the trace should be exactly 1.0. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - module.extra.backend = BackendKind::PauliSum; - module.extra.observable = Some("Z".to_string()); - // `Z0` matches a Z on qubit 0; the parser requires position anchors. - module.strings.push("Z0".to_string()); - - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::String, - Value::String(0), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - for _ in 0..module.code.len() { - machine.step_once()?; - } - - assert_eq!(machine.trace_record(), vec![1.0]); - Ok(()) - } - - #[test] - fn paulisum_multi_term_observable_seeds_all_terms() -> eyre::Result<()> { - // Task 11: a sum-valued observable seeds every term. With - // `"ZZ + 0.5*XX"` the state holds `1.0 * ZZ + 0.5 * XX`; tracing - // `[XZ]0[XZ]1` matches both words and returns 1.0 + 0.5 = 1.5. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 2; - module.extra.backend = BackendKind::PauliSum; - module.extra.observable = Some("ZZ + 0.5*XX".to_string()); - module.strings.push("[XZ]0[XZ]1".to_string()); - - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::String, - Value::String(0), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - for _ in 0..module.code.len() { - machine.step_once()?; - } - - assert_eq!(machine.trace_record(), vec![1.5]); - Ok(()) - } - - #[test] - fn paulisum_init_rejects_missing_observable() { - // Task 8 requires `device circuit.observable` for PauliSum / Lossy. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - module.extra.backend = BackendKind::PauliSum; - - let mut machine = PPVM::default(); - machine.load(&module).unwrap(); - let err = machine.init().unwrap_err(); - assert!( - err.to_string().contains("observable"), - "expected observable-related error, got: {err}" - ); - } - - #[test] - fn paulisum_init_rejects_mismatched_observable_length() { - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 2; - module.extra.backend = BackendKind::PauliSum; - // Three letters but only two qubits — should error. - module.extra.observable = Some("ZZZ".to_string()); - - let mut machine = PPVM::default(); - machine.load(&module).unwrap(); - let err = machine.init().unwrap_err(); - assert!( - err.to_string().contains("invalid Pauli-sum"), - "expected parser rejection, got: {err}" - ); - } - - #[test] - fn tableau_truncate_is_silent_no_op() -> eyre::Result<()> { - // Task 9: `circuit::circuit.truncate` on the default Tableau backend should run - // without error — the tableau prunes via coefficient_threshold during - // every gate, so the explicit Truncate instruction has nothing to do. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - // backend defaults to Tableau; no observable needed. - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::Truncate)); - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - machine.step_once()?; - // No observer effects emitted by Truncate. - assert!(machine.measurement_record().is_empty()); - assert!(machine.trace_record().is_empty()); - Ok(()) - } - - #[test] - fn apply_circuit_instruction_preserves_pc_and_code_len() -> eyre::Result<()> { - use crate::measurements::MeasurementOutcome; - - // cpu::cpu.breakpoint; then measure q0. Step to the cpu::cpu.breakpoint, inject X, resume. - let src = "device circuit.n_qubits 1;\n\ - fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; - let mut m = PPVM::default(); - m.load_program(src)?; - m.init()?; - - // Run until the cpu::cpu.breakpoint pauses us. - loop { - if m.step_once()? == StepOutcome::Breakpoint { - break; - } - } - let pc = m.current_pc(); - let len = m.loader.module.code.len(); - let depth = m.cpu.stack().len(); - - // Inject X on q0 while "paused". - m.apply_circuit_instruction(CircuitInstruction::X, &[0], &[])?; - - // The debugger's position must be untouched. - assert_eq!(m.current_pc(), pc, "pc must be preserved"); - assert_eq!( - m.loader.module.code.len(), - len, - "appended op must be truncated back" - ); - assert_eq!( - m.cpu.stack().len(), - depth, - "operand stack must be left unchanged" - ); - - // And the X took effect: resuming the program measures |1>. - while !matches!(m.step_once()?, StepOutcome::Return | StepOutcome::Halt) {} - let rec = m.measurement_record(); - assert_eq!(rec.len(), 1); - assert_eq!(rec[0].as_slice(), [MeasurementOutcome::One]); - Ok(()) - } - - #[test] - fn apply_circuit_instruction_preserves_pc_and_code_on_error() -> eyre::Result<()> { - // A program stepped to a known pc; an injected gate that errors mid-block - // must NOT corrupt the code vector or the program counter. - let src = "device circuit.n_qubits 1;\n\ - fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; - let mut m = PPVM::default(); - m.load_program(src)?; - m.init()?; - loop { - if m.step_once()? == StepOutcome::Breakpoint { - break; - } - } - let pc = m.current_pc(); - let len = m.loader.module.code.len(); - let depth = m.cpu.stack().len(); - - // RX with no float param errors during execution: resolve_circuit pops - // the f64 first and finds the qubit value (U64) instead, returning Err. - let err = m.apply_circuit_instruction(CircuitInstruction::RX, &[0], &[]); - assert!(err.is_err(), "expected the injected gate to error"); - - // The failed injection must leave the debugger untouched. - assert_eq!(m.current_pc(), pc, "pc must be preserved on error"); - assert_eq!( - m.loader.module.code.len(), - len, - "code must be truncated back on error" - ); - assert_eq!( - m.cpu.stack().len(), - depth, - "operand stack must be left unchanged on error" - ); - Ok(()) - } - - #[test] - fn apply_circuit_instruction_measurement_does_not_grow_the_stack() -> eyre::Result<()> { - // `circuit::circuit.measure` pushes its outcome onto the CPU operand stack for - // bytecode to consume. An injected measurement has no such consumer, so - // that push must be rolled back: otherwise a paused program resumes with - // a stray operand, and a REPL session's stack grows without bound. - let mut m = PPVM::with_qubits(1)?; - let depth = m.cpu.stack().len(); - m.apply_circuit_instruction(CircuitInstruction::Measure, &[0], &[])?; - assert_eq!( - m.cpu.stack().len(), - depth, - "injected measurement must not leave its outcome on the stack" - ); - // The outcome still lands in the measurement record. - assert_eq!(m.measurement_record().len(), 1); - Ok(()) - } - - #[test] - fn tableau_trace_emits_expectation_on_zero_state() { - // Task 16: `circuit::circuit.trace` on the Tableau backend now computes - // Σ_{P matches pat} ⟨ψ|P|ψ⟩ via `GeneralizedTableau::trace`. On the - // freshly-initialized |0⟩ state, pattern `Z0` matches the single - // Pauli Z and ⟨0|Z|0⟩ = 1, so the trace_record gets one entry: 1.0. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - module.strings.push("Z0".to_string()); - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::String, - Value::String(0), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); - - let mut machine = PPVM::default(); - machine.load(&module).unwrap(); - machine.init().unwrap(); - machine.step_once().unwrap(); // const.string - machine.step_once().unwrap(); // circuit::circuit.trace - let trace = machine.trace_record(); - assert_eq!(trace.len(), 1); - assert!( - (trace[0] - 1.0).abs() < 1e-12, - "expected ⟨0|Z|0⟩ = 1.0, got {}", - trace[0] - ); - } - - #[test] - fn paulisum_reset_restores_seeded_observable() -> eyre::Result<()> { - use vihaco::Reset; - - // Seed the observable `Z` (PauliSum backend), then apply H(0), which - // conjugates Z -> X in the Heisenberg picture and changes the state. - // reset() must rebuild the state from the seeded observable. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - module.extra.backend = BackendKind::PauliSum; - module.extra.observable = Some("Z".to_string()); - - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(0), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::H)); - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - - let seeded = machine.state_string(); - for _ in 0..module.code.len() { - machine.step_once()?; - } - assert_ne!( - machine.state_string(), - seeded, - "H(0) should have changed the propagated observable" - ); - - machine.reset(); - assert_eq!( - machine.state_string(), - seeded, - "reset must rebuild the state from the seeded observable" - ); - Ok(()) - } - - #[test] - fn lossy_paulisum_reset_restores_seeded_observable() -> eyre::Result<()> { - use vihaco::Reset; - - // Same as `paulisum_reset_restores_seeded_observable`, but through the - // LossyPauliSum dispatch path. - let mut module: LocalModule = - LocalModule::default(); - module.extra.n_qubits = 1; - module.extra.backend = BackendKind::LossyPauliSum; - module.extra.observable = Some("Z".to_string()); - - module - .code - .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( - Type::U64, - Value::U64(0), - ))); - module - .code - .push(PPVMInstruction::Circuit(CircuitInstruction::H)); - - let mut machine = PPVM::default(); - machine.load(&module)?; - machine.init()?; - - let seeded = machine.state_string(); - for _ in 0..module.code.len() { - machine.step_once()?; - } - assert_ne!( - machine.state_string(), - seeded, - "H(0) should have changed the propagated observable" - ); - - machine.reset(); - assert_eq!( - machine.state_string(), - seeded, - "reset must rebuild the state from the seeded observable" - ); - Ok(()) - } -} +// pub(crate) use self::ppvm as ppvm_module; + +// #[derive(Debug, Clone)] +// pub enum PPVMEffect { +// Step(StepOutcome), +// Stdout(StdoutEffect), +// Circuit(Box), +// Measurement(MeasurementEffect), +// Trace(TraceEffect), +// } + +// #[observe(vihaco::observer::stdio::StdoutEffect, effect = PPVMEffect)] +// impl PPVM { +// fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> eyre::Result> { +// Observe::::observe(&mut self.stdout, effect) +// } +// } + +// #[observe(CircuitEffect, effect = PPVMEffect)] +// impl PPVM { +// fn observe_circuit_effect( +// &mut self, +// effect: &CircuitEffect, +// ) -> eyre::Result> { +// Observe::::observe(&mut self.circuit, effect) +// } +// } + +// #[observe(MeasurementEffect, effect = PPVMEffect)] +// impl PPVM { +// fn observe_measurement_effect( +// &mut self, +// effect: &MeasurementEffect, +// ) -> eyre::Result> { +// Observe::::observe(&mut self.measurement_record, effect) +// } +// } + +// #[observe(TraceEffect, effect = PPVMEffect)] +// impl PPVM { +// fn observe_trace_effect(&mut self, effect: &TraceEffect) -> eyre::Result> { +// Observe::::observe(&mut self.trace_record, effect) +// } +// } + +// impl From for PPVMEffect { +// fn from(value: StdoutEffect) -> Self { +// Self::Stdout(value) +// } +// } + +// impl From for PPVMEffect { +// fn from(value: MeasurementEffect) -> Self { +// Self::Measurement(value) +// } +// } + +// impl From for PPVMEffect { +// fn from(value: TraceEffect) -> Self { +// Self::Trace(value) +// } +// } + +// impl From for PPVMEffect { +// fn from(value: CircuitOutcomeEffect) -> Self { +// match value { +// CircuitOutcomeEffect::Measurement(m) => Self::Measurement(m), +// CircuitOutcomeEffect::Trace(t) => Self::Trace(t), +// } +// } +// } + +// impl std::fmt::Display for PPVMInstruction { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// match self { +// PPVMInstruction::Cpu(inst) => inst.fmt(f), +// PPVMInstruction::Circuit(inst) => inst.fmt(f), +// } +// } +// } + +// impl PartialEq for PPVMInstruction { +// fn eq(&self, other: &Self) -> bool { +// match (self, other) { +// (PPVMInstruction::Cpu(a), PPVMInstruction::Cpu(b)) => a == b, +// (PPVMInstruction::Circuit(a), PPVMInstruction::Circuit(b)) => a == b, +// _ => false, +// } +// } +// } + +// impl From for PPVMInstruction { +// fn from(value: vihaco_cpu::RuntimeInstruction) -> Self { +// Self::Cpu(value) +// } +// } + +// impl From for PPVMInstruction { +// fn from(value: CircuitInstruction) -> Self { +// Self::Circuit(value) +// } +// } + +// impl PPVM { +// fn resolve_cpu(&mut self, inst: &vihaco_cpu::RuntimeInstruction) -> eyre::Result { +// match inst { +// vihaco_cpu::RuntimeInstruction::IndirectCall => { +// let function_id: u32 = self.cpu.stack_top()?.get_function_ref()?; +// let function = self.loader.get_function(function_id as usize)?; +// Ok(CPUMessage::FunctionInfo { +// arity: function.signature.params.len() as u32, +// start_address: function.start_address, +// }) +// } +// vihaco_cpu::RuntimeInstruction::Print => { +// let value = *self.cpu.stack_top()?; +// match value { +// vihaco::Value::String(addr) => { +// let string = self.loader.get_string(addr as usize)?.clone(); +// Ok(CPUMessage::Print(string)) +// } +// value => Ok(CPUMessage::Print(value.to_string())), +// } +// } +// _ => Ok(CPUMessage::None), +// } +// } + +// fn resolve_circuit(&mut self, inst: &CircuitInstruction) -> eyre::Result { +// use CircuitInstruction::*; +// match inst { +// X | Y | Z | H | S | SAdj | SqrtX | SqrtY | SqrtXAdj | SqrtYAdj | T | TAdj | Measure +// | Reset => { +// let q = self.pop_u64()?; +// Ok(CircuitMessage::Qubit(q)) +// } +// CNOT | CZ => { +// let q1 = self.pop_u64()?; +// let q0 = self.pop_u64()?; +// Ok(CircuitMessage::TwoQubit(q0, q1)) +// } +// RX | RY | RZ | Depolarize | Loss => { +// let theta = self.pop_f64()?; +// let q = self.pop_u64()?; +// Ok(CircuitMessage::QubitAndFloat(q, theta)) +// } +// Leakage => { +// // Push order: qubit, p0, p1. Pop reverse. +// let p1 = self.pop_f64()?; +// let p0 = self.pop_f64()?; +// let q = self.pop_u64()?; +// Ok(CircuitMessage::QubitAndTwoFloats(q, p0, p1)) +// } +// RXX | RYY | RZZ | Depolarize2 => { +// let theta = self.pop_f64()?; +// let q1 = self.pop_u64()?; +// let q0 = self.pop_u64()?; +// Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) +// } +// R => { +// let theta = self.pop_f64()?; +// let axis_angle = self.pop_f64()?; +// let q = self.pop_u64()?; +// Ok(CircuitMessage::QubitAndTwoFloats(q, axis_angle, theta)) +// } +// U3 => { +// let lam = self.pop_f64()?; +// let phi = self.pop_f64()?; +// let theta = self.pop_f64()?; +// let q = self.pop_u64()?; +// Ok(CircuitMessage::QubitU3(q, theta, phi, lam)) +// } + +// // TODO: pop actual float arrays? +// PauliError => { +// let pz = self.pop_f64()?; +// let py = self.pop_f64()?; +// let px = self.pop_f64()?; +// let q = self.pop_u64()?; +// Ok(CircuitMessage::QubitAndFloatArr3(q, [px, py, pz])) +// } +// CorrelatedLoss => { +// let p2 = self.pop_f64()?; +// let p1 = self.pop_f64()?; +// let p0 = self.pop_f64()?; +// let q1 = self.pop_u64()?; +// let q0 = self.pop_u64()?; +// Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) +// } +// TwoQubitPauliError => { +// let mut ps = [0.0; 15]; +// for p in ps.iter_mut().rev() { +// *p = self.pop_f64()?; +// } +// let q1 = self.pop_u64()?; +// let q0 = self.pop_u64()?; +// Ok(CircuitMessage::TwoQubitAndFloatArr15(q0, q1, ps)) +// } +// Trace => { +// let s = self.pop_string()?; +// Ok(CircuitMessage::PauliPatternStr(s)) +// } +// Truncate => Ok(CircuitMessage::None), +// } +// } + +// fn pop_u64(&mut self) -> eyre::Result { +// match self.cpu.stack_pop()? { +// vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into), +// v => Err(eyre::eyre!("Expected u64, got {:?}", v)), +// } +// } + +// fn pop_f64(&mut self) -> eyre::Result { +// match self.cpu.stack_pop()? { +// vihaco::Value::F64(v) => Ok(v), +// v => Err(eyre::eyre!("Expected f64 argument, got {:?}", v)), +// } +// } + +// /// Pop a `Value::String(addr)`, look up the addr in the module's string +// /// table, and return the owned string. Used by `Trace` to resolve its +// /// Pauli-pattern operand before the executor sees the message. +// fn pop_string(&mut self) -> eyre::Result { +// match self.cpu.stack_pop()? { +// vihaco::Value::String(addr) => Ok(self.loader.get_string(addr as usize)?.clone()), +// v => Err(eyre::eyre!("Expected string operand, got {:?}", v)), +// } +// } + +// pub fn init(&mut self) -> eyre::Result<()> { +// self.init_inner(None) +// } + +// /// Like [`PPVM::init`], but seed the circuit RNG deterministically so the +// /// run is reproducible. +// pub fn init_with_seed(&mut self, seed: u64) -> eyre::Result<()> { +// self.init_inner(Some(seed)) +// } + +// fn init_inner(&mut self, seed: Option) -> eyre::Result<()> { +// let info = &self.loader.module.extra; +// if info.n_qubits == 0 { +// return Err(eyre::eyre!("device circuit.n_qubits must be declared")); +// } +// self.circuit = match (info.backend, seed) { +// (BackendKind::Tableau, None) => Circuit::tableau(info)?, +// (BackendKind::Tableau, Some(seed)) => Circuit::tableau_with_seed(info, seed)?, +// (BackendKind::PauliSum, _) => { +// let terms = parse_observable_terms(info)?; +// Circuit::paulisum(info, &terms)? +// } +// (BackendKind::LossyPauliSum, _) => { +// let terms = parse_observable_terms(info)?; +// Circuit::lossy_paulisum(info, &terms)? +// } +// }; + +// // push entry frame +// self.cpu.push_frame(Frame { +// base: 0, +// span: (0, 0, 0), +// function: None, +// ret_pc: 0, +// }); + +// Ok(()) +// } + +// pub fn load( +// &mut self, +// module: &vihaco::module::LocalModule< +// PPVMInstruction, +// vihaco::Value, +// vihaco::Type, +// PPVMDeviceInfo, +// >, +// ) -> eyre::Result<()> { +// self.loader.module = module.clone(); +// Ok(()) +// } + +// pub fn step_once(&mut self) -> eyre::Result { +// let inst = self.loader.peek_instruction()?.clone(); +// let effects = self.execute_effects(inst)?; +// self.continue_effects(effects) +// } + +// /// Program counter: the index of the next instruction to execute. +// pub fn current_pc(&self) -> u32 { +// self.loader.pc() +// } + +// /// The next instruction to execute, or `None` once execution has run off +// /// the end of the code. Intended for debuggers/inspection. +// pub fn current_instruction(&self) -> Option { +// self.loader.peek_instruction().ok().cloned() +// } + +// /// Append one REPL command's lowered VM ops and run just that block against +// /// the persistent state, advancing the pc through it. NOTE: an out-of-range +// /// qubit index *panics* in the tableau rather than erroring, so callers must +// /// bounds-check qubit operands against `n_qubits` first. +// pub fn execute_single_instruction(&mut self, instrs: &[PPVMInstruction]) -> eyre::Result<()> { +// let start = self.loader.module.code.len() as u32; +// self.loader.module.code.extend_from_slice(instrs); +// *self.loader.pc_mut() = start; +// for _ in 0..instrs.len() { +// self.step_once()?; +// } +// Ok(()) +// } + +// /// Render the current circuit state (tableau / Pauli sum) for the REPL's +// /// `show` command. Delegates to the circuit's size-specific tableau. +// pub fn state_string(&self) -> String { +// self.circuit.state_string() +// } + +// /// Build a fresh, initialized `n_qubits`-qubit device with no code. The +// /// REPL's `device` command uses this to (re)create the machine. Errors if +// /// `n_qubits` is zero (a device must have at least one qubit). +// pub fn with_qubits(n_qubits: usize) -> eyre::Result { +// let mut machine = Self::default(); +// let mut module = vihaco::module::LocalModule::< +// PPVMInstruction, +// Value, +// vihaco::Type, +// PPVMDeviceInfo, +// >::default(); +// module.extra.n_qubits = n_qubits; +// machine.load(&module)?; +// machine.init()?; +// Ok(machine) +// } + +// /// Lower a single circuit instruction — qubit operands first, then float +// /// params, per the push-in-order / pop-in-reverse convention — and execute +// /// it against the persistent state. Qubit indices are bounds-checked against +// /// the device size first, because the tableau panics (rather than erroring) +// /// on an out-of-range qubit. +// pub fn apply_circuit_instruction( +// &mut self, +// inst: CircuitInstruction, +// qubits: &[usize], +// params: &[f64], +// ) -> eyre::Result<()> { +// let n_qubits = self.loader.module.extra.n_qubits; +// for &q in qubits { +// if q >= n_qubits { +// eyre::bail!("qubit {q} out of range for {n_qubits}-qubit device"); +// } +// } + +// let mut instrs = Vec::with_capacity(qubits.len() + params.len() + 1); +// for &q in qubits { +// instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(q as u64), +// ))); +// } +// for &p in params { +// instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::F64, +// Value::F64(p), +// ))); +// } +// instrs.push(PPVMInstruction::Circuit(inst)); + +// // Run the appended block, then roll back pc, code, and operand stack (on +// // every path, including error) so the injected op is transparent to a +// // paused program — only its tableau/measurement effects persist. +// let saved_pc = self.loader.pc(); +// let saved_len = self.loader.module.code.len(); +// let saved_stack = self.cpu.stack_len(); +// let result = self.execute_single_instruction(&instrs); +// self.loader.module.code.truncate(saved_len); +// *self.loader.pc_mut() = saved_pc; +// self.cpu.stack_mut().truncate(saved_stack); +// result +// } + +// fn execute_effects(&mut self, inst: PPVMInstruction) -> eyre::Result> { +// log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); +// match inst { +// PPVMInstruction::Cpu(cpu_inst) => { +// let msg = self.resolve_cpu(&cpu_inst)?; +// self.cpu.set_current_pc(self.loader.pc()); +// let stdout_effect = match (&cpu_inst, &msg) { +// ( +// vihaco_cpu::RuntimeInstruction::Print, +// vihaco_cpu::CPUMessage::Print(text), +// ) => Some(PPVMEffect::Stdout(StdoutEffect(text.clone()))), +// _ => None, +// }; +// let outcome = vihaco::expect_exactly_one_effect( +// vihaco::GeneratedComponent::execute_generated(&mut self.cpu, cpu_inst, msg)?, +// )?; +// // Advance past a cpu::cpu.breakpoint as well, so the debugger that paused +// // on it doesn't re-hit the same instruction on the next step. +// if matches!(outcome, StepOutcome::Continue | StepOutcome::Breakpoint) { +// if let Some(target) = self.cpu.take_pending_pc() { +// *self.loader.pc_mut() = target; +// } else { +// *self.loader.pc_mut() += 1; +// } +// } +// let mut effects = Effects::one(PPVMEffect::Step(outcome)); +// if let Some(stdout_effect) = stdout_effect { +// effects = effects.append(stdout_effect); +// } +// Ok(effects) +// } +// PPVMInstruction::Circuit(inst) => { +// let msg = self.resolve_circuit(&inst)?; +// let circuit_effects = ::execute_generated( +// &mut self.circuit, +// inst, +// msg, +// )?; +// *self.loader.pc_mut() += 1; +// let mut effects = Effects::one(PPVMEffect::Step(StepOutcome::Continue)); +// for outcome in circuit_effects { +// effects = effects.append(PPVMEffect::from(outcome)); +// } +// Ok(effects) +// } +// } +// } + +// fn continue_effects(&mut self, effects: Effects) -> eyre::Result { +// let mut step_outcome = None; +// for effect in effects { +// match effect { +// PPVMEffect::Step(outcome) => { +// if step_outcome.replace(outcome).is_some() { +// return Err(eyre::eyre!( +// "expected exactly one PPVM step effect, got multiple" +// )); +// } +// } +// effect => self.continue_observer_effect(effect)?, +// } +// } + +// step_outcome.ok_or_else(|| eyre::eyre!("expected exactly one PPVM step effect, got 0")) +// } + +// fn continue_observer_effect(&mut self, effect: PPVMEffect) -> eyre::Result<()> { +// match effect { +// PPVMEffect::Stdout(effect) => { +// let follow_ups = Observe::::observe(self, &effect)?; +// self.continue_observer_effects(follow_ups) +// } +// PPVMEffect::Circuit(effect) => { +// let follow_ups = Observe::::observe(self, &effect)?; +// self.continue_observer_effects(follow_ups) +// } +// PPVMEffect::Measurement(effect) => { +// let follow_ups = Observe::::observe(self, &effect)?; +// // NOTE: push measurement outcomes onto the stack as u32 (0/1/2, where 2 = Lost). +// for outcome in effect.measurement_results { +// let m = Value::U32(outcome as u32); +// self.cpu.stack_push(m); +// } +// self.continue_observer_effects(follow_ups) +// } +// PPVMEffect::Trace(effect) => { +// let value = effect.value; +// let follow_ups = Observe::::observe(self, &effect)?; +// // Mirror the measurement wiring: append to the trace record +// // (via the observer above) AND push the value onto the CPU +// // stack so user bytecode can consume it. Plan Task 7. +// self.cpu.stack_push(Value::F64(value)); +// self.continue_observer_effects(follow_ups) +// } +// PPVMEffect::Step(_) => Err(eyre::eyre!( +// "unexpected Step effect while continuing PPVM observer follow-ups" +// )), +// } +// } + +// fn continue_observer_effects(&mut self, effects: Effects) -> eyre::Result<()> { +// for effect in effects { +// self.continue_observer_effect(effect)?; +// } +// Ok(()) +// } + +// pub fn run(&mut self) -> eyre::Result { +// self.run_with_seed(None) +// } + +// /// Like [`PPVM::run`], but seed the circuit RNG deterministically when +// /// `seed` is `Some`, making the run reproducible. +// pub fn run_with_seed(&mut self, seed: Option) -> eyre::Result { +// match seed { +// Some(seed) => self.init_with_seed(seed)?, +// None => self.init()?, +// } + +// loop { +// // Breakpoints only pause the interactive debugger; a batch run +// // skips straight past them. +// match self.step_once()? { +// StepOutcome::Continue | StepOutcome::Breakpoint => continue, +// action => return Ok(action), +// } +// } +// } + +// pub fn stdout(&self) -> &[u8] { +// self.stdout.output() +// } + +// pub fn measurement_record(&self) -> Vec { +// self.measurement_record.record.clone() +// } + +// /// Per-trace values collected by `Trace` instructions during the run. +// /// Parallel to [`PPVM::measurement_record`]: one f64 per `Trace` executed, +// /// in execution order. +// pub fn trace_record(&self) -> Vec { +// self.trace_record.record.clone() +// } + +// pub fn load_program(&mut self, program: &str) -> eyre::Result<()> { +// let module = crate::compile_program(program)?; +// self.load(&module)?; +// Ok(()) +// } + +// /// Load from a file, auto-detecting the format: if it starts with the PPVM +// /// magic it is loaded as `.ssb` bytecode, otherwise it is parsed as `.sst` +// /// source text. A magic match commits to the bytecode path — a corrupt +// /// `.ssb` errors rather than silently falling back to the text parser. +// pub fn load_file(&mut self, path: &str) -> eyre::Result<()> { +// let bytes = std::fs::read(path)?; +// if crate::bytecode::is_bytecode(&bytes) { +// self.load_bytecode(&bytes) +// } else { +// self.load_program(std::str::from_utf8(&bytes)?) +// } +// } + +// /// Load a module from an in-memory `.ssb` byte stream. +// pub fn load_bytecode(&mut self, bytes: &[u8]) -> eyre::Result<()> { +// let module = crate::bytecode::module_from_bytes(bytes)?; +// self.load(&module) +// } + +// /// Read a `.ssb` file and load the module it contains. +// pub fn load_bytecode_file(&mut self, path: &str) -> eyre::Result<()> { +// let bytes = std::fs::read(path)?; +// self.load_bytecode(&bytes) +// } + +// pub fn run_program(&mut self, program: &str) -> eyre::Result<()> { +// self.load_program(program)?; +// self.run()?; +// Ok(()) +// } + +// pub fn run_file(&mut self, path: &str) -> eyre::Result<()> { +// self.load_file(path)?; +// self.run()?; +// Ok(()) +// } +// } + +// impl vihaco::Reset for PPVM { +// fn reset(&mut self) { +// self.cpu.reset(); +// self.circuit.reset(); +// self.loader.pc = 0; +// self.measurement_record.record.clear(); +// self.trace_record.record.clear(); +// } +// } + +// /// Parse the `device circuit.observable` header into Pauli-sum terms ready to +// /// seed a `PauliSum` / `LossyPauliSum` state. Single-Pauli observables from +// /// Phase 2 keep working as the degenerate one-term case; multi-term sums like +// /// `"1.0*ZZ + 0.5*XX"` are handled by [`parse_pauli_sum_terms`]. +// fn parse_observable_terms(info: &PPVMDeviceInfo) -> Result> { +// let observable = info.observable.as_deref().ok_or_else(|| { +// eyre!( +// "the {:?} backend requires `device circuit.observable` to be set", +// info.backend +// ) +// })?; +// crate::observable::parse_pauli_sum_terms(observable, info.n_qubits) +// } + +// #[cfg(test)] +// mod tests { +// use vihaco::{Type, Value, module::LocalModule}; + +// use super::*; + +// #[test] +// fn test_run_ppvm() -> eyre::Result<()> { +// let mut module: LocalModule = +// LocalModule::default(); + +// module.extra.n_qubits = 2; + +// /* +// cpu::cpu.const u64, 0 +// circuit::circuit.h +// */ +// let zero = PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(0), +// )); +// let one = PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(1), +// )); +// module.code.push(zero.clone()); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + +// /* +// cpu::cpu.const u64, 0 +// circuit::circuit.t +// */ +// module.code.push(zero.clone()); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::T)); + +// /* +// cpu::cpu.const u64, 0 +// cpu::cpu.const u64, 1 +// circuit::circuit.cnot +// */ +// module.code.push(zero.clone()); +// module.code.push(one.clone()); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::CNOT)); + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; + +// for _ in 0..module.code.len() { +// machine.step_once()?; +// assert!(machine.cpu.stack().len() <= 2); +// } + +// let num_coefficients = match &machine.circuit { +// Circuit::Tableau(TableauCircuit::Bits64(ex)) => { +// println!("{}", ex.tab); +// ex.tab.coefficients.len() +// } +// Circuit::Tableau(TableauCircuit::Bits128(ex)) => { +// println!("{}", ex.tab); +// ex.tab.coefficients.len() +// } +// Circuit::Tableau(TableauCircuit::Bits256(ex)) => { +// println!("{}", ex.tab); +// ex.tab.coefficients.len() +// } +// Circuit::Tableau(TableauCircuit::Bits512(ex)) => { +// println!("{}", ex.tab); +// ex.tab.coefficients.len() +// } +// Circuit::Tableau(TableauCircuit::Bits1024(ex)) => { +// println!("{}", ex.tab); +// ex.tab.coefficients.len() +// } +// Circuit::Tableau(TableauCircuit::Bits2048(ex)) => { +// println!("{}", ex.tab); +// ex.tab.coefficients.len() +// } +// Circuit::PauliSum(_) | Circuit::LossyPauliSum(_) => { +// panic!("test expects the default Tableau backend, got a PauliSum variant"); +// } +// }; + +// assert_eq!(num_coefficients, 2); +// Ok(()) +// } + +// #[test] +// fn test_device_decl() -> eyre::Result<()> { +// // Equivalent .sst source: +// // +// // device circuit.n_qubits 5; +// // device circuit.coefficient_threshold 1e-10; +// // +// // fn @main() { ...5-qubit GHZ + 5 measurements... } + +// let mut module: LocalModule = +// LocalModule::default(); + +// module.extra.n_qubits = 5; +// module.extra.coefficient_threshold = 1e-10; + +// // 5-qubit GHZ: H on q0, then CNOT(q_i, q_{i+1}) for i = 0..4. +// /* +// cpu::cpu.const u64, 0 +// circuit::circuit.h +// */ +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(0), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + +// for i in 0..4u64 { +// /* +// cpu::cpu.const u64, i +// cpu::cpu.const u64, i+1 +// circuit::circuit.cnot +// */ +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(i), +// ))); +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(i + 1), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::CNOT)); +// } + +// // Measure all 5 qubits. +// for q in 0..5u64 { +// /* +// cpu::cpu.const u64, q +// circuit::circuit.measure +// */ +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(q), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::Measure)); +// } + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; + +// for _ in 0..module.code.len() { +// machine.step_once()?; +// } + +// assert_eq!(machine.measurement_record().len(), 5); +// Ok(()) +// } + +// // ─── Incremental execution (REPL) ───────────────────────────────────── + +// #[test] +// fn execute_single_instruction_persists_state_across_calls() -> eyre::Result<()> { +// use crate::measurements::MeasurementOutcome; + +// // A 1-qubit device with no code; the REPL builds up instructions +// // incrementally, one command at a time, rather than loading a program. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; + +// // First command: X on q0 (|0> -> |1>). +// let x = [ +// PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(0), +// )), +// PPVMInstruction::Circuit(CircuitInstruction::X), +// ]; +// machine.execute_single_instruction(&x)?; +// // No measurement yet. +// assert!(machine.measurement_record().is_empty()); + +// // Second command: measure q0. The X from the first command must persist, +// // so the outcome is deterministically |1>. +// let measure = [ +// PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(0), +// )), +// PPVMInstruction::Circuit(CircuitInstruction::Measure), +// ]; +// machine.execute_single_instruction(&measure)?; + +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 1); +// assert_eq!(record[0].as_slice(), [MeasurementOutcome::One]); +// Ok(()) +// } + +// #[test] +// fn execute_single_instruction_propagates_engine_errors() -> eyre::Result<()> { +// // The REPL relies on engine errors surfacing as `Err` (so it can print +// // them and keep looping) rather than panicking. A circuit with no qubit +// // operand on the stack is one such propagating error. +// // +// // NOTE: an out-of-range qubit index (>= n_qubits) currently *panics* in +// // the tableau rather than erroring, so the REPL command layer must +// // bounds-check qubit indices before calling `execute`. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; + +// // `circuit::circuit.h` with nothing on the stack: `pop_u64` fails. +// let missing_operand = [PPVMInstruction::Circuit(CircuitInstruction::H)]; +// assert!( +// machine +// .execute_single_instruction(&missing_operand) +// .is_err() +// ); +// Ok(()) +// } + +// #[test] +// fn state_string_renders_a_small_device() -> eyre::Result<()> { +// let source = "device circuit.n_qubits 2;\nfn @main() { cpu::cpu.ret 0 }\n"; +// let mut machine = PPVM::default(); +// machine.load_program(source)?; +// machine.init()?; + +// let rendered = machine.state_string(); +// assert!( +// !rendered.is_empty(), +// "state_string should render the tableau" +// ); +// // PPVM delegates to the circuit. +// assert_eq!(rendered, machine.circuit.state_string()); +// Ok(()) +// } + +// #[test] +// fn resolve_circuit_pops_operands_in_reverse_of_push_order() -> eyre::Result<()> { +// // Convention: operands are pushed in argument order (q0, q1, then any +// // floats) and popped in reverse. So every two-qubit circuit must read q0 as +// // the first operand pushed, consistently, with or without trailing +// // floats. (CNOT already obeyed this; the float-carrying arms did not.) +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 8; +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; + +// // CNOT: push q0=2, q1=5. +// machine.cpu.stack_push(Value::U64(2)); +// machine.cpu.stack_push(Value::U64(5)); +// assert_eq!( +// machine.resolve_circuit(&CircuitInstruction::CNOT)?, +// CircuitMessage::TwoQubit(2, 5) +// ); + +// // RXX: push q0=2, q1=5, theta — same qubit order as CNOT. +// machine.cpu.stack_push(Value::U64(2)); +// machine.cpu.stack_push(Value::U64(5)); +// machine.cpu.stack_push(Value::F64(0.3)); +// assert_eq!( +// machine.resolve_circuit(&CircuitInstruction::RXX)?, +// CircuitMessage::TwoQubitAndFloat(2, 5, 0.3) +// ); + +// // CorrelatedLoss: push q0=2, q1=5, p0, p1, p2. +// machine.cpu.stack_push(Value::U64(2)); +// machine.cpu.stack_push(Value::U64(5)); +// machine.cpu.stack_push(Value::F64(0.1)); +// machine.cpu.stack_push(Value::F64(0.2)); +// machine.cpu.stack_push(Value::F64(0.3)); +// assert_eq!( +// machine.resolve_circuit(&CircuitInstruction::CorrelatedLoss)?, +// CircuitMessage::TwoQubitAndFloatArr3(2, 5, [0.1, 0.2, 0.3]) +// ); + +// // Leakage: push q=2, p0, p1 — two floats, unlike Loss. +// machine.cpu.stack_push(Value::U64(2)); +// machine.cpu.stack_push(Value::F64(0.1)); +// machine.cpu.stack_push(Value::F64(0.2)); +// assert_eq!( +// machine.resolve_circuit(&CircuitInstruction::Leakage)?, +// CircuitMessage::QubitAndTwoFloats(2, 0.1, 0.2) +// ); +// Ok(()) +// } + +// #[test] +// fn with_qubits_builds_an_initialized_device() -> eyre::Result<()> { +// use crate::measurements::MeasurementOutcome; + +// let mut m = PPVM::with_qubits(2)?; +// // The device is ready to take instructions immediately. +// m.apply_circuit_instruction(CircuitInstruction::X, &[0], &[])?; +// m.apply_circuit_instruction(CircuitInstruction::Measure, &[0], &[])?; +// let record = m.measurement_record(); +// assert_eq!(record.len(), 1); +// assert_eq!(record[0].as_slice(), [MeasurementOutcome::One]); +// Ok(()) +// } + +// #[test] +// fn with_qubits_zero_is_an_error() { +// assert!(PPVM::with_qubits(0).is_err()); +// } + +// #[test] +// fn apply_circuit_instruction_bounds_checks_qubits() -> eyre::Result<()> { +// // q1 is out of range on a 1-qubit device: this must error rather than +// // panic in the tableau. +// let mut m = PPVM::with_qubits(1)?; +// let err = m +// .apply_circuit_instruction(CircuitInstruction::X, &[1], &[]) +// .unwrap_err(); +// assert!(err.to_string().contains("out of range"), "got: {err}"); +// Ok(()) +// } + +// // ─── Parser-driven entry points ─────────────────────────────────────── + +// #[test] +// fn load_program_populates_device_info_and_code() -> eyre::Result<()> { +// let source = "device circuit.n_qubits 2;\n\ +// device circuit.coefficient_threshold 1e-8;\n\ +// fn @main() {\n\ +// cpu::cpu.const u64, 0\n\ +// circuit::circuit.h\n\ +// cpu::cpu.ret 0\n\ +// }\n"; +// let mut machine = PPVM::default(); +// machine.load_program(source)?; +// assert_eq!(machine.loader.module.extra.n_qubits, 2); +// assert_eq!(machine.loader.module.extra.coefficient_threshold, 1e-8); +// // cpu::cpu.const u64, 0 / circuit::circuit.h / ret = 3 +// assert_eq!(machine.loader.module.code.len(), 3); +// Ok(()) +// } + +// #[test] +// fn run_program_executes_bell_circuit() -> eyre::Result<()> { +// let source = "device circuit.n_qubits 2;\n\ +// fn @main() {\n\ +// cpu::cpu.const u64, 0\n\ +// circuit::circuit.h\n\ +// cpu::cpu.const u64, 0\n\ +// cpu::cpu.const u64, 1\n\ +// circuit::circuit.cnot\n\ +// cpu::cpu.const u64, 0\n\ +// circuit::circuit.measure\n\ +// cpu::cpu.const u64, 1\n\ +// circuit::circuit.measure\n\ +// cpu::cpu.ret 0\n\ +// }\n"; +// let mut machine = PPVM::default(); +// machine.run_program(source)?; +// let record = machine.measurement_record(); +// assert_eq!(record.len(), 2); +// Ok(()) +// } + +// #[test] +// fn reset_clears_the_measurement_record() -> eyre::Result<()> { +// use vihaco::Reset; + +// let source = "device circuit.n_qubits 2;\n\ +// fn @main() {\n\ +// cpu::cpu.const u64, 0\n\ +// circuit::circuit.h\n\ +// cpu::cpu.const u64, 0\n\ +// cpu::cpu.const u64, 1\n\ +// circuit::circuit.cnot\n\ +// cpu::cpu.const u64, 0\n\ +// circuit::circuit.measure\n\ +// cpu::cpu.const u64, 1\n\ +// circuit::circuit.measure\n\ +// cpu::cpu.ret 0\n\ +// }\n"; +// let mut machine = PPVM::default(); +// machine.run_program(source)?; +// assert_eq!(machine.measurement_record().len(), 2); + +// // Resetting the machine must discard the recorded measurements, so a +// // subsequent run does not see stale results leaking in from before. +// machine.reset(); +// assert!( +// machine.measurement_record().is_empty(), +// "reset must clear the measurement record" +// ); + +// Ok(()) +// } + +// #[test] +// fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { +// let source = "fn @main() { cpu::cpu.ret 0 }\n"; +// let mut machine = PPVM::default(); +// machine.load_program(source)?; +// let err = machine.init().unwrap_err(); +// assert!(err.to_string().contains("circuit.n_qubits"), "err: {err}"); +// Ok(()) +// } + +// #[test] +// fn run_program_reports_parse_errors() { +// let source = "device circuit.n_qubits 2;\n\ +// fn @main() {\n\ +// circuit::circuit.not_a_real_gate\n\ +// cpu::cpu.ret 0\n\ +// }\n"; +// let mut machine = PPVM::default(); +// let err = machine.run_program(source).unwrap_err(); +// assert!( +// err.to_string().contains("parsing failed") +// || err.to_string().contains("parsing functions failed") +// || err.to_string().contains("unhandled raw form"), +// "err: {err}" +// ); +// } + +// // ─── Breakpoints ────────────────────────────────────────────────────── + +// /// Bell circuit with a `cpu::cpu.breakpoint` between the two measurements. +// const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 2;\n\ +// fn @main() {\n\ +// cpu::cpu.const u64, 0\n\ +// circuit::circuit.h\n\ +// cpu::cpu.const u64, 0\n\ +// cpu::cpu.const u64, 1\n\ +// circuit::circuit.cnot\n\ +// cpu::cpu.const u64, 0\n\ +// circuit::circuit.measure\n\ +// cpu::cpu.breakpoint\n\ +// cpu::cpu.const u64, 1\n\ +// circuit::circuit.measure\n\ +// cpu::cpu.ret 0\n\ +// }\n"; + +// #[test] +// fn run_ignores_breakpoints() -> eyre::Result<()> { +// // A batch run must execute straight through the cpu::cpu.breakpoint and record +// // both measurements, exactly as if it weren't there. +// let mut machine = PPVM::default(); +// machine.run_program(BREAKPOINT_PROGRAM)?; +// assert_eq!(machine.measurement_record().len(), 2); +// Ok(()) +// } + +// #[test] +// fn step_once_advances_past_breakpoint() -> eyre::Result<()> { +// let mut machine = PPVM::default(); +// machine.load_program(BREAKPOINT_PROGRAM)?; +// machine.init()?; + +// // Step until the cpu::cpu.breakpoint pauses us. +// let mut outcome = StepOutcome::Continue; +// for _ in 0..machine.loader.module.code.len() { +// outcome = machine.step_once()?; +// if outcome == StepOutcome::Breakpoint { +// break; +// } +// } +// assert_eq!( +// outcome, +// StepOutcome::Breakpoint, +// "cpu::cpu.breakpoint should pause" +// ); +// let pc_at_break = machine.current_pc(); + +// // Stepping again must make progress (advance the pc) rather than +// // re-hitting the same cpu::cpu.breakpoint instruction. +// let next = machine.step_once()?; +// assert_ne!( +// next, +// StepOutcome::Breakpoint, +// "must move past the cpu::cpu.breakpoint" +// ); +// assert!(machine.current_pc() > pc_at_break, "pc must advance"); + +// Ok(()) +// } + +// #[test] +// fn paulisum_truncate_runs_without_error() -> eyre::Result<()> { +// // Smoke test: a `circuit::circuit.truncate` reaches the PauliSum executor's +// // Truncate arm and calls `state.truncate()`. Task 8 makes the +// // observable mandatory for PauliSum init, so seed `Z` here. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; +// module.extra.backend = BackendKind::PauliSum; +// module.extra.observable = Some("Z".to_string()); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::Truncate)); + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; +// machine.step_once()?; +// Ok(()) +// } + +// #[test] +// fn paulisum_trace_populates_trace_record() -> eyre::Result<()> { +// // End-to-end Trace pipeline: with the observable `Z` seeded (Task 8), +// // tracing the `Z0` pattern picks up that one term with coefficient +// // 1.0, so the trace should be exactly 1.0. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; +// module.extra.backend = BackendKind::PauliSum; +// module.extra.observable = Some("Z".to_string()); +// // `Z0` matches a Z on qubit 0; the parser requires position anchors. +// module.strings.push("Z0".to_string()); + +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::String, +// Value::String(0), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; +// for _ in 0..module.code.len() { +// machine.step_once()?; +// } + +// assert_eq!(machine.trace_record(), vec![1.0]); +// Ok(()) +// } + +// #[test] +// fn paulisum_multi_term_observable_seeds_all_terms() -> eyre::Result<()> { +// // Task 11: a sum-valued observable seeds every term. With +// // `"ZZ + 0.5*XX"` the state holds `1.0 * ZZ + 0.5 * XX`; tracing +// // `[XZ]0[XZ]1` matches both words and returns 1.0 + 0.5 = 1.5. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 2; +// module.extra.backend = BackendKind::PauliSum; +// module.extra.observable = Some("ZZ + 0.5*XX".to_string()); +// module.strings.push("[XZ]0[XZ]1".to_string()); + +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::String, +// Value::String(0), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; +// for _ in 0..module.code.len() { +// machine.step_once()?; +// } + +// assert_eq!(machine.trace_record(), vec![1.5]); +// Ok(()) +// } + +// #[test] +// fn paulisum_init_rejects_missing_observable() { +// // Task 8 requires `device circuit.observable` for PauliSum / Lossy. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; +// module.extra.backend = BackendKind::PauliSum; + +// let mut machine = PPVM::default(); +// machine.load(&module).unwrap(); +// let err = machine.init().unwrap_err(); +// assert!( +// err.to_string().contains("observable"), +// "expected observable-related error, got: {err}" +// ); +// } + +// #[test] +// fn paulisum_init_rejects_mismatched_observable_length() { +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 2; +// module.extra.backend = BackendKind::PauliSum; +// // Three letters but only two qubits — should error. +// module.extra.observable = Some("ZZZ".to_string()); + +// let mut machine = PPVM::default(); +// machine.load(&module).unwrap(); +// let err = machine.init().unwrap_err(); +// assert!( +// err.to_string().contains("invalid Pauli-sum"), +// "expected parser rejection, got: {err}" +// ); +// } + +// #[test] +// fn tableau_truncate_is_silent_no_op() -> eyre::Result<()> { +// // Task 9: `circuit::circuit.truncate` on the default Tableau backend should run +// // without error — the tableau prunes via coefficient_threshold during +// // every gate, so the explicit Truncate instruction has nothing to do. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; +// // backend defaults to Tableau; no observable needed. +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::Truncate)); + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; +// machine.step_once()?; +// // No observer effects emitted by Truncate. +// assert!(machine.measurement_record().is_empty()); +// assert!(machine.trace_record().is_empty()); +// Ok(()) +// } + +// #[test] +// fn apply_circuit_instruction_preserves_pc_and_code_len() -> eyre::Result<()> { +// use crate::measurements::MeasurementOutcome; + +// // cpu::cpu.breakpoint; then measure q0. Step to the cpu::cpu.breakpoint, inject X, resume. +// let src = "device circuit.n_qubits 1;\n\ +// fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; +// let mut m = PPVM::default(); +// m.load_program(src)?; +// m.init()?; + +// // Run until the cpu::cpu.breakpoint pauses us. +// loop { +// if m.step_once()? == StepOutcome::Breakpoint { +// break; +// } +// } +// let pc = m.current_pc(); +// let len = m.loader.module.code.len(); +// let depth = m.cpu.stack().len(); + +// // Inject X on q0 while "paused". +// m.apply_circuit_instruction(CircuitInstruction::X, &[0], &[])?; + +// // The debugger's position must be untouched. +// assert_eq!(m.current_pc(), pc, "pc must be preserved"); +// assert_eq!( +// m.loader.module.code.len(), +// len, +// "appended op must be truncated back" +// ); +// assert_eq!( +// m.cpu.stack().len(), +// depth, +// "operand stack must be left unchanged" +// ); + +// // And the X took effect: resuming the program measures |1>. +// while !matches!(m.step_once()?, StepOutcome::Return | StepOutcome::Halt) {} +// let rec = m.measurement_record(); +// assert_eq!(rec.len(), 1); +// assert_eq!(rec[0].as_slice(), [MeasurementOutcome::One]); +// Ok(()) +// } + +// #[test] +// fn apply_circuit_instruction_preserves_pc_and_code_on_error() -> eyre::Result<()> { +// // A program stepped to a known pc; an injected gate that errors mid-block +// // must NOT corrupt the code vector or the program counter. +// let src = "device circuit.n_qubits 1;\n\ +// fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; +// let mut m = PPVM::default(); +// m.load_program(src)?; +// m.init()?; +// loop { +// if m.step_once()? == StepOutcome::Breakpoint { +// break; +// } +// } +// let pc = m.current_pc(); +// let len = m.loader.module.code.len(); +// let depth = m.cpu.stack().len(); + +// // RX with no float param errors during execution: resolve_circuit pops +// // the f64 first and finds the qubit value (U64) instead, returning Err. +// let err = m.apply_circuit_instruction(CircuitInstruction::RX, &[0], &[]); +// assert!(err.is_err(), "expected the injected gate to error"); + +// // The failed injection must leave the debugger untouched. +// assert_eq!(m.current_pc(), pc, "pc must be preserved on error"); +// assert_eq!( +// m.loader.module.code.len(), +// len, +// "code must be truncated back on error" +// ); +// assert_eq!( +// m.cpu.stack().len(), +// depth, +// "operand stack must be left unchanged on error" +// ); +// Ok(()) +// } + +// #[test] +// fn apply_circuit_instruction_measurement_does_not_grow_the_stack() -> eyre::Result<()> { +// // `circuit::circuit.measure` pushes its outcome onto the CPU operand stack for +// // bytecode to consume. An injected measurement has no such consumer, so +// // that push must be rolled back: otherwise a paused program resumes with +// // a stray operand, and a REPL session's stack grows without bound. +// let mut m = PPVM::with_qubits(1)?; +// let depth = m.cpu.stack().len(); +// m.apply_circuit_instruction(CircuitInstruction::Measure, &[0], &[])?; +// assert_eq!( +// m.cpu.stack().len(), +// depth, +// "injected measurement must not leave its outcome on the stack" +// ); +// // The outcome still lands in the measurement record. +// assert_eq!(m.measurement_record().len(), 1); +// Ok(()) +// } + +// #[test] +// fn tableau_trace_emits_expectation_on_zero_state() { +// // Task 16: `circuit::circuit.trace` on the Tableau backend now computes +// // Σ_{P matches pat} ⟨ψ|P|ψ⟩ via `GeneralizedTableau::trace`. On the +// // freshly-initialized |0⟩ state, pattern `Z0` matches the single +// // Pauli Z and ⟨0|Z|0⟩ = 1, so the trace_record gets one entry: 1.0. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; +// module.strings.push("Z0".to_string()); +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::String, +// Value::String(0), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); + +// let mut machine = PPVM::default(); +// machine.load(&module).unwrap(); +// machine.init().unwrap(); +// machine.step_once().unwrap(); // const.string +// machine.step_once().unwrap(); // circuit::circuit.trace +// let trace = machine.trace_record(); +// assert_eq!(trace.len(), 1); +// assert!( +// (trace[0] - 1.0).abs() < 1e-12, +// "expected ⟨0|Z|0⟩ = 1.0, got {}", +// trace[0] +// ); +// } + +// #[test] +// fn paulisum_reset_restores_seeded_observable() -> eyre::Result<()> { +// use vihaco::Reset; + +// // Seed the observable `Z` (PauliSum backend), then apply H(0), which +// // conjugates Z -> X in the Heisenberg picture and changes the state. +// // reset() must rebuild the state from the seeded observable. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; +// module.extra.backend = BackendKind::PauliSum; +// module.extra.observable = Some("Z".to_string()); + +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(0), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; + +// let seeded = machine.state_string(); +// for _ in 0..module.code.len() { +// machine.step_once()?; +// } +// assert_ne!( +// machine.state_string(), +// seeded, +// "H(0) should have changed the propagated observable" +// ); + +// machine.reset(); +// assert_eq!( +// machine.state_string(), +// seeded, +// "reset must rebuild the state from the seeded observable" +// ); +// Ok(()) +// } + +// #[test] +// fn lossy_paulisum_reset_restores_seeded_observable() -> eyre::Result<()> { +// use vihaco::Reset; + +// // Same as `paulisum_reset_restores_seeded_observable`, but through the +// // LossyPauliSum dispatch path. +// let mut module: LocalModule = +// LocalModule::default(); +// module.extra.n_qubits = 1; +// module.extra.backend = BackendKind::LossyPauliSum; +// module.extra.observable = Some("Z".to_string()); + +// module +// .code +// .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( +// Type::U64, +// Value::U64(0), +// ))); +// module +// .code +// .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + +// let mut machine = PPVM::default(); +// machine.load(&module)?; +// machine.init()?; + +// let seeded = machine.state_string(); +// for _ in 0..module.code.len() { +// machine.step_once()?; +// } +// assert_ne!( +// machine.state_string(), +// seeded, +// "H(0) should have changed the propagated observable" +// ); + +// machine.reset(); +// assert_eq!( +// machine.state_string(), +// seeded, +// "reset must rebuild the state from the seeded observable" +// ); +// Ok(()) +// } +// } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index ec19595fe..8241c5496 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -3,8 +3,8 @@ // pub mod bytecode; pub mod component; +pub mod composite; pub mod device_info; -pub mod machine; pub mod measurements; pub mod observable; // pub mod shots; From 90d3b62d778e771583823ad809f6513a05c982b9 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 4 Nov 2026 11:22:27 +0100 Subject: [PATCH 07/36] Add some missing impls for PauliSum --- crates/ppvm-pauli-sum/src/sum/mod.rs | 1 + crates/ppvm-pauli-sum/src/sum/reset.rs | 142 +++++++++++++++++++++++++ crates/ppvm-pauli-sum/src/sum/rot1.rs | 103 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 crates/ppvm-pauli-sum/src/sum/reset.rs diff --git a/crates/ppvm-pauli-sum/src/sum/mod.rs b/crates/ppvm-pauli-sum/src/sum/mod.rs index 280dd5a60..b51e95e30 100644 --- a/crates/ppvm-pauli-sum/src/sum/mod.rs +++ b/crates/ppvm-pauli-sum/src/sum/mod.rs @@ -7,6 +7,7 @@ mod display; mod noise; mod ops; mod proj; +mod reset; mod rot1; mod rot2; mod trace; diff --git a/crates/ppvm-pauli-sum/src/sum/reset.rs b/crates/ppvm-pauli-sum/src/sum/reset.rs new file mode 100644 index 000000000..4215cf048 --- /dev/null +++ b/crates/ppvm-pauli-sum/src/sum/reset.rs @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::sum::PauliSum; +use ppvm_traits::char::Pauli; +use ppvm_traits::config::Config; +use ppvm_traits::traits::*; + +/// Reset to `|0⟩` in the Heisenberg picture. +/// +/// The adjoint of the reset channel maps `A_q ⊗ B` to `⟨0|A|0⟩ · I_q ⊗ B`: +/// `I` and `Z` on the reset qubit become `I`, while `X` and `Y` drop out. +/// Terms that collide after relabelling have their coefficients added. +/// Lost qubits are left untouched, matching the tableau, which skips +/// inactive qubits on reset; use `ResetLossChannel` to model reloading. +impl Reset for PauliSum { + fn reset(&mut self, addr0: usize) { + self.data_mut() + .retain(|k, _| k.get_lbit(addr0) || !k.get_xbit(addr0)); + self.map_add(|k, v| { + if !k.get_lbit(addr0) && k.get_zbit(addr0) { + (k.set_new(addr0, Pauli::I), v.clone()) + } else { + (k.clone(), v.clone()) + } + }); + } + + // The trait defaults run reset first (Schrödinger order); the Heisenberg + // picture needs the basis change applied before the reset instead. + fn reset_x(&mut self, addr0: usize) { + self.h(addr0); + self.reset(addr0); + } + + fn reset_y(&mut self, addr0: usize) { + self.s(addr0); + self.h(addr0); + self.reset(addr0); + } +} + +#[cfg(test)] +mod tests { + use crate::config::fxhash::ByteF64; + + use super::*; + + type Sum = PauliSum>; + + fn sum(n_qubits: usize, terms: &[(&str, f64)]) -> Sum { + let mut s: Sum = PauliSum::builder().n_qubits(n_qubits).build(); + for &(word, coef) in terms { + s += (word, coef); + } + s + } + + #[test] + fn test_reset_single_qubit_paulis() { + let mut s = sum(1, &[("I", 0.5)]); + s.reset(0); + assert_eq!(s, sum(1, &[("I", 0.5)])); + + let mut s = sum(1, &[("Z", 0.5)]); + s.reset(0); + assert_eq!(s, sum(1, &[("I", 0.5)])); + + let mut s = sum(1, &[("X", 0.5), ("Y", 0.25)]); + s.reset(0); + assert!(s.is_empty()); + } + + #[test] + fn test_reset_merges_colliding_terms() { + // Qubit 0 is the leftmost character. + let mut s = sum(2, &[("ZX", 0.5), ("IX", 0.25), ("YZ", 1.0)]); + s.reset(0); + assert_eq!(s, sum(2, &[("IX", 0.75)])); + } + + #[test] + fn test_reset_only_touches_target_qubit() { + let mut s = sum(2, &[("XZ", 1.0), ("ZY", 1.0)]); + s.reset(1); + assert_eq!(s, sum(2, &[("XI", 1.0)])); + } + + #[test] + fn test_reset_leaves_lost_qubit_untouched() { + use ppvm_pauli_word::loss::LossyPauliWord; + type Lossy = PauliSum< + crate::config::fxhash::Byte< + 1, + f64, + ppvm_traits::traits::NoStrategy, + LossyPauliWord<[u8; 1], fxhash::FxBuildHasher>, + >, + >; + let mut s: Lossy = PauliSum::builder().n_qubits(2).build(); + s += ("LZ", 1.0); + s += ("ZZ", 0.5); + s.reset(0); + let mut expect: Lossy = PauliSum::builder().n_qubits(2).build(); + expect += ("LZ", 1.0); + expect += ("IZ", 0.5); + assert_eq!(s, expect); + } + + #[test] + fn test_reset_after_x_gives_zero_state() { + // X then reset: ⟨Z⟩ = +1. Heisenberg order: reset, then X. + let mut o = sum(1, &[("Z", 1.0)]); + o.reset(0); + o.x(0); + assert_eq!(o, sum(1, &[("I", 1.0)])); + } + + #[test] + fn test_reset_x_prepares_plus_state() { + // reset_x then measure X: ⟨X⟩ = +1, ⟨Z⟩ = 0. + let mut o = sum(1, &[("X", 1.0)]); + o.reset_x(0); + assert_eq!(o, sum(1, &[("I", 1.0)])); + + let mut o = sum(1, &[("Z", 1.0)]); + o.reset_x(0); + assert!(o.is_empty()); + } + + #[test] + fn test_reset_y_prepares_plus_i_state() { + // reset_y then measure Y: ⟨Y⟩ = +1 on |+i⟩. + let mut o = sum(1, &[("Y", 1.0)]); + o.reset_y(0); + assert_eq!(o, sum(1, &[("I", 1.0)])); + + let mut o = sum(1, &[("X", 1.0)]); + o.reset_y(0); + assert!(o.is_empty()); + } +} diff --git a/crates/ppvm-pauli-sum/src/sum/rot1.rs b/crates/ppvm-pauli-sum/src/sum/rot1.rs index 3f21a6fb7..644386b3d 100644 --- a/crates/ppvm-pauli-sum/src/sum/rot1.rs +++ b/crates/ppvm-pauli-sum/src/sum/rot1.rs @@ -142,6 +142,33 @@ where } } +impl TGate for PauliSum +where + PauliSum: RotationOne, +{ + fn t(&mut self, addr0: usize) { + // T = e^{iπ/8} · RZ(π/4); the global phase drops out under conjugation. + self.rz(addr0, T::Coeff::from(std::f64::consts::FRAC_PI_4)); + } + + fn t_dag(&mut self, addr0: usize) { + self.rz(addr0, T::Coeff::from(-std::f64::consts::FRAC_PI_4)); + } +} + +impl U3Gate for PauliSum +where + PauliSum: RotationOne, +{ + fn u3(&mut self, addr: usize, theta: T::Coeff, phi: T::Coeff, lambda: T::Coeff) { + // U3(θ, φ, λ) = RZ(φ)·RY(θ)·RZ(λ). Heisenberg picture, so the + // sub-rotations run in reverse of the tableau's forward order. + self.rz(addr, phi); + self.ry(addr, theta); + self.rz(addr, lambda); + } +} + /// 2-bit Pauli code: 00 I, 01 X, 10 Z, 11 Y /// Returns \(ε, k\) so that –i \[P_i, P_j\]/2 = ε · P_k. /// For every commuting pair it yields (0, 0). @@ -180,6 +207,7 @@ pub fn levi_civita(i: u8, j: u8) -> (i8, u8) { #[cfg(test)] mod tests { use crate::config::fxhash::ByteF64; + use std::f64::consts::PI; use super::*; @@ -308,4 +336,79 @@ mod tests { expect += ("I", 1.0); assert_eq!(answer, expect); } + + /// `⟨0|O|0⟩` for a one-qubit observable: the I and Z coefficients. + fn zero_state_expectation(o: &PauliSum>) -> f64 { + let mut zero_state: PauliSum> = PauliSum::builder().n_qubits(1).build(); + zero_state += ("I", 1.0); + zero_state += ("Z", 1.0); + o.overlap(&zero_state) + } + + /// Heisenberg-evolve `pauli` back through `circuit` and evaluate on `|0⟩`. + fn expectation(pauli: &str, circuit: impl Fn(&mut PauliSum>)) -> f64 { + let mut o: PauliSum> = PauliSum::builder().n_qubits(1).build(); + o += (pauli, 1.0); + circuit(&mut o); + zero_state_expectation(&o) + } + + #[test] + fn test_t_on_plus_state() { + // T·H|0⟩ = (|0⟩ + e^{iπ/4}|1⟩)/√2, so ⟨X⟩ = cos(π/4), ⟨Y⟩ = sin(π/4). + // Gates are listed in reverse (Heisenberg) order: T first, then H. + let s = std::f64::consts::FRAC_1_SQRT_2; + let x = expectation("X", |o| { + o.t(0); + o.h(0); + }); + let y = expectation("Y", |o| { + o.t(0); + o.h(0); + }); + assert!((x - s).abs() < 1e-12, "⟨X⟩ = {x}"); + assert!((y - s).abs() < 1e-12, "⟨Y⟩ = {y}"); + + let y_dag = expectation("Y", |o| { + o.t_dag(0); + o.h(0); + }); + assert!((y_dag + s).abs() < 1e-12, "⟨Y⟩ after T† = {y_dag}"); + } + + #[test] + fn test_t_squared_is_s() { + let mut via_t: PauliSum> = PauliSum::builder().n_qubits(1).build(); + via_t += ("X", 1.0); + via_t.t(0); + via_t.t(0); + let mut via_s: PauliSum> = PauliSum::builder().n_qubits(1).build(); + via_s += ("X", 1.0); + via_s.s(0); + assert!((via_t.overlap(&via_s) - 1.0).abs() < 1e-12); + } + + #[test] + fn test_u3_on_zero_state() { + // U3(θ, φ, λ)|0⟩ = cos(θ/2)|0⟩ + e^{iφ} sin(θ/2)|1⟩ (λ only sets a phase). + let (theta, phi, lambda) = (0.34 * PI, 0.21 * PI, 0.46 * PI); + let u3 = |o: &mut PauliSum>| o.u3(0, theta, phi, lambda); + let x = expectation("X", u3); + let y = expectation("Y", u3); + let z = expectation("Z", u3); + assert!((x - theta.sin() * phi.cos()).abs() < 1e-12, "⟨X⟩ = {x}"); + assert!((y - theta.sin() * phi.sin()).abs() < 1e-12, "⟨Y⟩ = {y}"); + assert!((z - theta.cos()).abs() < 1e-12, "⟨Z⟩ = {z}"); + } + + #[test] + fn test_u3_on_plus_state() { + // On |+⟩ the λ phase becomes visible: ⟨Z⟩ = -sin θ · cos λ. + let (theta, phi, lambda) = (0.34 * PI, 0.21 * PI, 0.46 * PI); + let z = expectation("Z", |o| { + o.u3(0, theta, phi, lambda); + o.h(0); + }); + assert!((z + theta.sin() * lambda.cos()).abs() < 1e-12, "⟨Z⟩ = {z}"); + } } From 8c7e1c6b4c148eda58f15d2c0807430a297c3019 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 4 Nov 2026 12:00:48 +0100 Subject: [PATCH 08/36] Implement backend trait for PauliSum --- crates/ppvm-vihaco/src/component.rs | 47 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 7e585b11d..84cab4351 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -83,8 +83,7 @@ pub trait CircuitBackend { + PauliError + TwoQubitPauliError + LossChannel - + CorrelatedLossChannel - + LeakageChannel; + + CorrelatedLossChannel; fn state(&mut self) -> &mut Self::State; @@ -94,18 +93,10 @@ pub trait CircuitBackend { Err(eyre!("measure is not supported on this backend")) } - fn leakage(&mut self, _addr0: usize, p0: f64, p1: f61) -> eyre::Result<()> { + fn leakage(&mut self, _addr0: usize, p0: f64, p1: f64) -> eyre::Result<()> { Err(eyre!("leakage is not supported on this backend")) } - fn loss(&mut self, _addr0: usize, p: f64) -> eyre::Result<()> { - Err(eyre!("loss is not supported on this backend")) - } - - fn correlated_loss(&mut self, _addr0: usize, p0: f64, p1: f64, p2: f64) -> eyre::Result<()> { - Err(eyre!("loss is not supported on this backend")) - } - fn truncate(&mut self); fn reset(&mut self); @@ -145,9 +136,29 @@ where fn reset(&mut self) { self.reset_all(); } + + fn leakage(&mut self, _addr0: usize, p0: f64, p1: f64) -> eyre::Result<()> { + self.leakage_channel(_addr0, p0, p1); + Ok(()) + } +} + +/// PauliSum that also stores the initial observable so we can reset the component +pub struct PauliSumBackend> { + pub state: PauliSum, + initial_state: PauliSum, +} + +impl> PauliSumBackend { + pub fn new(observable: PauliSum) -> Self { + Self { + state: observable.clone(), + initial_state: observable.clone(), + } + } } -impl CircuitBackend for PauliSum +impl CircuitBackend for PauliSumBackend where T: Config, { @@ -155,11 +166,15 @@ where type State = PauliSum; fn state(&mut self) -> &mut Self::State { - self + &mut self.state } fn reset(&mut self) { - unimplemented!("TODO") + self.state = self.initial_state.clone(); + } + + fn truncate(&mut self) { + self.state.truncate(); } } @@ -288,9 +303,7 @@ impl Execute for CircuitExecutor { type Error = eyre::Report; fn execute(&mut self, _: &circuit::Leakage, msg: Self::Message) -> eyre::Result<()> { - self.backend - .state() - .leakage_channel(msg.0.0, msg.0.1, msg.0.2); + self.backend.leakage(msg.0.0, msg.0.1, msg.0.2)?; Ok(()) } } From 3b9f77aa1cf3d7d004c013aadd5306407982d04c Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 4 Nov 2026 14:15:22 +0100 Subject: [PATCH 09/36] Implement stepping on composite --- crates/ppvm-vihaco/src/composite.rs | 108 +++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 0bd7236d1..a8af43af4 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -2,17 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use eyre::{Result, eyre}; +use ppvm_pauli_sum::config::fx64hash::Byte8F64; +use ppvm_tableau::data::GeneralizedTableau; +use vihaco::PushToStack; use vihaco::frame::Frame; use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; use vihaco::traits::{GetProgramInfo, ProgramCounter, StackMemory}; use vihaco::{ - Effects, NoContext, Observe, ProgramImage, Step, Type, Value, composite, observe, program, + BuildMessage, Effects, Execute, ExecutionState, HasInstructionSet, NoContext, Observe, + ProgramImage, Step, Type, Value, composite, field, module::LocalModule, observe, program, }; use vihaco_cpu::CPU; use crate::component::{CircuitBackend, CircuitExecutor}; -use crate::measurements::{MeasurementObserver, TraceObserver}; +use crate::measurements::{CircuitOutcomeEffect, MeasurementObserver, TraceObserver}; use vihaco_circuit_isa::circuit; @@ -42,6 +46,106 @@ pub struct PPVM { trace_record: TraceObserver, } +type Instruction = as HasInstructionSet>::Runtime; + +type SourceInstruction = as HasInstructionSet>::Syntax; + +type Program = LocalModule, Value, Type>; + +// TODO: this workaround relies on internals and should be fixed upstream +type ExecMarker = __vihaco_fields_PPVM::__markers::execution; +type CpuMarker = __vihaco_fields_PPVM::__markers::cpu; +type CircuitMarker = __vihaco_fields_PPVM::__markers::circuit; + +// execution stepping +impl Step> for PPVM +where + Self: BuildMessage>, + >::Child: Execute, +{ + fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { + let message = >>::build_message( + self, + instruction, + module, + )?; + >::get_mut(self).execute(instruction, message) + } +} + +// CPU stepping +impl Step> for PPVM +where + Self: BuildMessage>, + >::Child: Execute, + ExecutionState: + vihaco::Apply<<>::Child as Execute>::Effect>, +{ + fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { + let message = >>::build_message( + self, + instruction, + module, + )?; + let effect = + >::get_mut(self).execute(instruction, message)?; + vihaco::Apply::apply(&mut self.execution, effect); + Ok(()) + } +} + +// handling circuit effects +impl vihaco::Apply> for PPVM { + fn apply(&mut self, effects: Effects) { + for effect in effects { + match effect { + CircuitOutcomeEffect::Measurement(m) => { + self.measurement_record.record.push(m.measurement_results) + } + CircuitOutcomeEffect::Trace(t) => { + self.trace_record.record.push(t.value); + } + } + } + } +} + +impl vihaco::Apply<()> for PPVM { + fn apply(&mut self, _effect: ()) {} +} + +// Circuit stepping +impl Step> for PPVM +where + Self: BuildMessage>, + >::Child: Execute, + Self: vihaco::Apply<<>::Child as Execute>::Effect>, +{ + fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { + let message = >>::build_message( + self, + instruction, + module, + )?; + let effect = + >::get_mut(self).execute(instruction, message)?; + vihaco::Apply::apply(self, effect); + Ok(()) + } +} + +pub type TableauBackend64 = GeneralizedTableau>; + +#[cfg(test)] +#[allow(dead_code)] +fn assert_dispatch_coverage( + m: &mut PPVM, + inst: &Instruction, + module: &Program, +) -> eyre::Result<()> { + vihaco::Dispatch::>::dispatch(m, inst, module) +} + // pub(crate) use self::ppvm as ppvm_module; // #[derive(Debug, Clone)] From f0ce27c811ea0468aae52ebd603d8770ceab28cc Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 4 Nov 2026 14:25:26 +0100 Subject: [PATCH 10/36] Don't pop usize --- crates/ppvm-vihaco/src/component.rs | 61 ++++++++++++++++------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 84cab4351..fdb2cdc8e 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -188,11 +188,11 @@ macro_rules! single_qubit_execute { ($leaf:ident, $method:ident) => { impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack; + type Message = PopFromStack<(u64,)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().$method(msg.0); + self.backend.state().$method(msg.0.0 as usize); Ok(()) } } @@ -203,11 +203,13 @@ macro_rules! two_qubit_execute { ($leaf:ident, $method:ident) => { impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, usize)>; + type Message = PopFromStack<(u64, u64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().$method(msg.0.0, msg.0.1); + self.backend + .state() + .$method(msg.0.0 as usize, msg.0.1 as usize); Ok(()) } } @@ -218,11 +220,11 @@ macro_rules! single_qubit_rotation_execute { ($leaf:ident, $method:ident) => { impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, f64)>; + type Message = PopFromStack<(u64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().$method(msg.0.0, msg.0.1); + self.backend.state().$method(msg.0.0 as usize, msg.0.1); Ok(()) } } @@ -233,11 +235,13 @@ macro_rules! two_qubit_rotation_execute { ($leaf:ident, $method:ident) => { impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, usize, f64)>; + type Message = PopFromStack<(u64, u64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().$method(msg.0.0, msg.0.1, msg.0.2); + self.backend + .state() + .$method(msg.0.0 as usize, msg.0.1 as usize, msg.0.2); Ok(()) } } @@ -277,82 +281,85 @@ two_qubit_rotation_execute!(Depolarize2, depolarize2); impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, f64, f64, f64)>; + type Message = PopFromStack<(u64, f64, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::U3, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().u3(msg.0.0, msg.0.1, msg.0.2, msg.0.3); + self.backend + .state() + .u3(msg.0.0 as usize, msg.0.1, msg.0.2, msg.0.3); Ok(()) } } impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, f64, f64)>; + type Message = PopFromStack<(u64, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::R, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().r(msg.0.0, msg.0.1, msg.0.2); + self.backend.state().r(msg.0.0 as usize, msg.0.1, msg.0.2); Ok(()) } } impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, f64, f64)>; + type Message = PopFromStack<(u64, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::Leakage, msg: Self::Message) -> eyre::Result<()> { - self.backend.leakage(msg.0.0, msg.0.1, msg.0.2)?; + self.backend.leakage(msg.0.0 as usize, msg.0.1, msg.0.2)?; Ok(()) } } impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, [f64; 3])>; + type Message = PopFromStack<(u64, f64, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::PauliError, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().pauli_error(msg.0.0, msg.0.1); + self.backend + .state() + .pauli_error(msg.0.0 as usize, [msg.0.1, msg.0.2, msg.0.3]); Ok(()) } } impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, usize, [f64; 15])>; + type Message = PopFromStack<(u64, u64, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::TwoQubitPauliError, msg: Self::Message) -> eyre::Result<()> { // TODO: probably needs a custom message since we can't pop float arrays - self.backend - .state() - .two_qubit_pauli_error(msg.0.0, msg.0.1, msg.0.2); - Ok(()) + unimplemented!("TODO: needs a custom message") } } impl Execute for CircuitExecutor { type Effect = (); - type Message = PopFromStack<(usize, usize, [f64; 3])>; + type Message = PopFromStack<(u64, u64, f64, f64, f64)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::CorrelatedLoss, msg: Self::Message) -> eyre::Result<()> { - self.backend - .state() - .correlated_loss_channel(msg.0.0, msg.0.1, msg.0.2); + self.backend.state().correlated_loss_channel( + msg.0.0 as usize, + msg.0.1 as usize, + [msg.0.2, msg.0.3, msg.0.4], + ); Ok(()) } } impl Execute for CircuitExecutor { type Effect = Effects; - type Message = PopFromStack; + type Message = PopFromStack<(u64,)>; type Error = eyre::Report; fn execute(&mut self, _: &circuit::Measure, msg: Self::Message) -> eyre::Result { - let outcome = self.backend.measure(msg.0)?; + let outcome = self.backend.measure(msg.0.0 as usize)?; Ok(Effects::one(CircuitOutcomeEffect::Measurement( MeasurementEffect { measurement_results: smallvec::smallvec![outcome], From 812d56334d830f093eec3ba0dfb606398a8fc0e1 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 11:57:54 +0100 Subject: [PATCH 11/36] Bring back enums --- crates/ppvm-vihaco/src/component.rs | 255 ++++++++++++++++++++++------ crates/ppvm-vihaco/src/composite.rs | 4 - 2 files changed, 202 insertions(+), 57 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index fdb2cdc8e..fa72681f9 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -14,7 +14,7 @@ use ppvm_pauli_sum::config::fx64hash::Byte8F64; use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; use ppvm_tableau::prelude::*; -use vihaco::{Effects, Execute, PopFromStack, component, dispatch, observe}; +use vihaco::{Apply, Component, Effects, Execute, PopFromStack, component, dispatch, observe}; use vihaco_circuit_isa::circuit; /// Largest qubit count any backend can simulate. The widest size bucket is @@ -184,15 +184,33 @@ pub struct CircuitExecutor { pub backend: B, } +// CircuitOp trait: define how an instruction executes on a generic backend +// This is used to avoid having huge where constraints on the enums later +// - without this trait, we'd have to constrain every enum variant to all Execute impls +// TODO: re-evaluate whether this is needed once we refactor and try to get rid of enums +pub trait CircuitOp { + type Message; + type Effect; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result; +} + macro_rules! single_qubit_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor { + impl CircuitOp for circuit::$leaf { type Effect = (); type Message = PopFromStack<(u64,)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().$method(msg.0.0 as usize); + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend.state().$method(msg.0.0 as usize); Ok(()) } } @@ -201,15 +219,16 @@ macro_rules! single_qubit_execute { macro_rules! two_qubit_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor { + impl CircuitOp for circuit::$leaf { type Effect = (); type Message = PopFromStack<(u64, u64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend - .state() - .$method(msg.0.0 as usize, msg.0.1 as usize); + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend.state().$method(msg.0.0 as usize, msg.0.1 as usize); Ok(()) } } @@ -218,13 +237,16 @@ macro_rules! two_qubit_execute { macro_rules! single_qubit_rotation_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor { + impl CircuitOp for circuit::$leaf { type Effect = (); type Message = PopFromStack<(u64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().$method(msg.0.0 as usize, msg.0.1); + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend.state().$method(msg.0.0 as usize, msg.0.1); Ok(()) } } @@ -233,13 +255,16 @@ macro_rules! single_qubit_rotation_execute { macro_rules! two_qubit_rotation_execute { ($leaf:ident, $method:ident) => { - impl Execute for CircuitExecutor { + impl CircuitOp for circuit::$leaf { type Effect = (); type Message = PopFromStack<(u64, u64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::$leaf, msg: Self::Message) -> eyre::Result<()> { - self.backend + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend .state() .$method(msg.0.0 as usize, msg.0.1 as usize, msg.0.2); Ok(()) @@ -279,72 +304,66 @@ two_qubit_rotation_execute!(Depolarize2, depolarize2); // TODO: batched gates; need custom messages -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::U3 { type Effect = (); type Message = PopFromStack<(u64, f64, f64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::U3, msg: Self::Message) -> eyre::Result<()> { - self.backend + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend .state() .u3(msg.0.0 as usize, msg.0.1, msg.0.2, msg.0.3); Ok(()) } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::R { type Effect = (); type Message = PopFromStack<(u64, f64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::R, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().r(msg.0.0 as usize, msg.0.1, msg.0.2); + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend.state().r(msg.0.0 as usize, msg.0.1, msg.0.2); Ok(()) } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::Leakage { type Effect = (); type Message = PopFromStack<(u64, f64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::Leakage, msg: Self::Message) -> eyre::Result<()> { - self.backend.leakage(msg.0.0 as usize, msg.0.1, msg.0.2)?; + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend.leakage(msg.0.0 as usize, msg.0.1, msg.0.2)?; Ok(()) } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::PauliError { type Effect = (); type Message = PopFromStack<(u64, f64, f64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::PauliError, msg: Self::Message) -> eyre::Result<()> { - self.backend + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend .state() .pauli_error(msg.0.0 as usize, [msg.0.1, msg.0.2, msg.0.3]); Ok(()) } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::TwoQubitPauliError { type Effect = (); type Message = PopFromStack<(u64, u64, f64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::TwoQubitPauliError, msg: Self::Message) -> eyre::Result<()> { + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { // TODO: probably needs a custom message since we can't pop float arrays unimplemented!("TODO: needs a custom message") } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::CorrelatedLoss { type Effect = (); type Message = PopFromStack<(u64, u64, f64, f64, f64)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::CorrelatedLoss, msg: Self::Message) -> eyre::Result<()> { - self.backend.state().correlated_loss_channel( + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend.state().correlated_loss_channel( msg.0.0 as usize, msg.0.1 as usize, [msg.0.2, msg.0.3, msg.0.4], @@ -353,13 +372,16 @@ impl Execute for CircuitExecutor } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::Measure { type Effect = Effects; type Message = PopFromStack<(u64,)>; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::Measure, msg: Self::Message) -> eyre::Result { - let outcome = self.backend.measure(msg.0.0 as usize)?; + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result { + let outcome = backend.measure(msg.0.0 as usize)?; Ok(Effects::one(CircuitOutcomeEffect::Measurement( MeasurementEffect { measurement_results: smallvec::smallvec![outcome], @@ -368,23 +390,21 @@ impl Execute for CircuitExecutor { } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::Truncate { type Effect = (); type Message = vihaco::NoMessage; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::Truncate, _: Self::Message) -> eyre::Result<()> { - self.backend.truncate(); + fn apply(&self, backend: &mut B, _: Self::Message) -> eyre::Result<()> { + backend.truncate(); Ok(()) } } -impl Execute for CircuitExecutor { +impl CircuitOp for circuit::Trace { type Effect = (); type Message = vihaco::NoMessage; - type Error = eyre::Report; - fn execute(&mut self, _: &circuit::Trace, _: Self::Message) -> eyre::Result<()> { + fn apply(&self, backend: &mut B, _: Self::Message) -> eyre::Result<()> { unimplemented!("TODO: needs a custom message"); } } @@ -394,3 +414,132 @@ impl vihaco::Reset for CircuitExecutor { self.backend.reset(); } } + +// The single execute impl for all CircuitOps +impl Execute for CircuitExecutor { + type Effect = I::Effect; + type Message = I::Message; + type Error = eyre::Report; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result { + I::apply(instruction, &mut self.backend, message) + } +} + +// TODO: enum dispatch can go once we only set the index type in tableau #204 +pub(crate) enum TableauBuckets { + Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>>), + Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>>), + Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>>), + Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>>), + Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>>), + Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>>), +} + +impl TableauBuckets { + fn apply(&mut self, op: &I, msg: I::Message) -> eyre::Result { + match self { + Self::Bits64(c) => op.apply(&mut c.backend, msg), + Self::Bits128(c) => op.apply(&mut c.backend, msg), + Self::Bits256(c) => op.apply(&mut c.backend, msg), + Self::Bits512(c) => op.apply(&mut c.backend, msg), + Self::Bits1024(c) => op.apply(&mut c.backend, msg), + Self::Bits2048(c) => op.apply(&mut c.backend, msg), + } + } +} + +pub(crate) enum PauliSumBuckets { + Bits64(CircuitExecutor>>), + Bits128(CircuitExecutor>>), + Bits256(CircuitExecutor>>), + Bits512(CircuitExecutor>>), + Bits1024(CircuitExecutor>>), + Bits2048(CircuitExecutor>>), +} + +impl PauliSumBuckets { + fn apply(&mut self, op: &I, msg: I::Message) -> eyre::Result { + match self { + Self::Bits64(c) => op.apply(&mut c.backend, msg), + Self::Bits128(c) => op.apply(&mut c.backend, msg), + Self::Bits256(c) => op.apply(&mut c.backend, msg), + Self::Bits512(c) => op.apply(&mut c.backend, msg), + Self::Bits1024(c) => op.apply(&mut c.backend, msg), + Self::Bits2048(c) => op.apply(&mut c.backend, msg), + } + } +} + +pub(crate) enum LossyPauliSumBuckets { + Bits64(CircuitExecutor>>), + Bits128(CircuitExecutor>>), + Bits256(CircuitExecutor>>), + Bits512(CircuitExecutor>>), + Bits1024(CircuitExecutor>>), + Bits2048(CircuitExecutor>>), +} + +impl LossyPauliSumBuckets { + fn apply(&mut self, op: &I, msg: I::Message) -> eyre::Result { + match self { + Self::Bits64(c) => op.apply(&mut c.backend, msg), + Self::Bits128(c) => op.apply(&mut c.backend, msg), + Self::Bits256(c) => op.apply(&mut c.backend, msg), + Self::Bits512(c) => op.apply(&mut c.backend, msg), + Self::Bits1024(c) => op.apply(&mut c.backend, msg), + Self::Bits2048(c) => op.apply(&mut c.backend, msg), + } + } +} + +pub(crate) enum CircuitKind { + Tableau(TableauBuckets), + PauliSum(PauliSumBuckets), + LossyPauliSum(LossyPauliSumBuckets), +} + +#[component] +#[instructions { circuit }] +pub struct Circuit { + inner: CircuitKind, +} + +impl Execute for Circuit { + type Message = I::Message; + type Effect = I::Effect; + type Error = eyre::Report; + + fn execute(&mut self, instruction: &I, msg: I::Message) -> Result { + match &mut self.inner { + CircuitKind::Tableau(b) => b.apply(instruction, msg), + CircuitKind::PauliSum(b) => b.apply(instruction, msg), + CircuitKind::LossyPauliSum(b) => b.apply(instruction, msg), + } + } +} + +// Tableau-only component +#[component] +#[instructions { circuit }] +pub struct TableauCircuit { + inner: TableauBuckets, +} + +impl Execute for TableauCircuit { + type Message = I::Message; + type Effect = I::Effect; + type Error = eyre::Report; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result { + self.inner.apply(instruction, message) + } +} diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index a8af43af4..5a18327a9 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -39,8 +39,6 @@ pub struct PPVM { #[device(2)] circuit: crate::component::CircuitExecutor, - stdout: StdoutObserver, - measurement_record: MeasurementObserver, trace_record: TraceObserver, @@ -48,8 +46,6 @@ pub struct PPVM { type Instruction = as HasInstructionSet>::Runtime; -type SourceInstruction = as HasInstructionSet>::Syntax; - type Program = LocalModule, Value, Type>; // TODO: this workaround relies on internals and should be fixed upstream From 19cdf7903bf8bbbcfb0ec54caf5802d6dc043c5e Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 12:41:08 +0100 Subject: [PATCH 12/36] Remove a bunch of old code --- crates/ppvm-vihaco/src/component.rs | 55 +++ crates/ppvm-vihaco/src/composite.rs | 674 ++-------------------------- 2 files changed, 95 insertions(+), 634 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index fa72681f9..c3d838fde 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -451,6 +451,17 @@ impl TableauBuckets { Self::Bits2048(c) => op.apply(&mut c.backend, msg), } } + + fn reset(&mut self) { + match self { + Self::Bits64(c) => vihaco::Reset::reset(c), + Self::Bits128(c) => vihaco::Reset::reset(c), + Self::Bits256(c) => vihaco::Reset::reset(c), + Self::Bits512(c) => vihaco::Reset::reset(c), + Self::Bits1024(c) => vihaco::Reset::reset(c), + Self::Bits2048(c) => vihaco::Reset::reset(c), + } + } } pub(crate) enum PauliSumBuckets { @@ -473,6 +484,17 @@ impl PauliSumBuckets { Self::Bits2048(c) => op.apply(&mut c.backend, msg), } } + + fn reset(&mut self) { + match self { + Self::Bits64(c) => vihaco::Reset::reset(c), + Self::Bits128(c) => vihaco::Reset::reset(c), + Self::Bits256(c) => vihaco::Reset::reset(c), + Self::Bits512(c) => vihaco::Reset::reset(c), + Self::Bits1024(c) => vihaco::Reset::reset(c), + Self::Bits2048(c) => vihaco::Reset::reset(c), + } + } } pub(crate) enum LossyPauliSumBuckets { @@ -495,6 +517,17 @@ impl LossyPauliSumBuckets { Self::Bits2048(c) => op.apply(&mut c.backend, msg), } } + + fn reset(&mut self) { + match self { + Self::Bits64(c) => vihaco::Reset::reset(c), + Self::Bits128(c) => vihaco::Reset::reset(c), + Self::Bits256(c) => vihaco::Reset::reset(c), + Self::Bits512(c) => vihaco::Reset::reset(c), + Self::Bits1024(c) => vihaco::Reset::reset(c), + Self::Bits2048(c) => vihaco::Reset::reset(c), + } + } } pub(crate) enum CircuitKind { @@ -503,6 +536,16 @@ pub(crate) enum CircuitKind { LossyPauliSum(LossyPauliSumBuckets), } +impl CircuitKind { + fn reset(&mut self) { + match self { + Self::Tableau(b) => b.reset(), + Self::PauliSum(b) => b.reset(), + Self::LossyPauliSum(b) => b.reset(), + } + } +} + #[component] #[instructions { circuit }] pub struct Circuit { @@ -543,3 +586,15 @@ impl Execute for TableauCircuit { self.inner.apply(instruction, message) } } + +impl vihaco::Reset for Circuit { + fn reset(&mut self) { + self.inner.reset() + } +} + +impl vihaco::Reset for TableauCircuit { + fn reset(&mut self) { + self.inner.reset() + } +} diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 5a18327a9..756cfd295 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -15,7 +15,7 @@ use vihaco::{ }; use vihaco_cpu::CPU; -use crate::component::{CircuitBackend, CircuitExecutor}; +use crate::component::{Circuit, CircuitBackend, CircuitExecutor, CircuitOp}; use crate::measurements::{CircuitOutcomeEffect, MeasurementObserver, TraceObserver}; use vihaco_circuit_isa::circuit; @@ -27,7 +27,7 @@ pub use crate::device_info::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo}; // #[derive(Default)] #[composite] -pub struct PPVM { +pub struct PPVM { #[device(0)] #[program] execution: vihaco_stdlib::ExecutionState, @@ -37,61 +37,54 @@ pub struct PPVM { // NOTE: we need the full import path here for some vihaco macro magic reasons #[device(2)] - circuit: crate::component::CircuitExecutor, + circuit: crate::component::Circuit, measurement_record: MeasurementObserver, trace_record: TraceObserver, } -type Instruction = as HasInstructionSet>::Runtime; +type Instruction = ::Runtime; -type Program = LocalModule, Value, Type>; - -// TODO: this workaround relies on internals and should be fixed upstream -type ExecMarker = __vihaco_fields_PPVM::__markers::execution; -type CpuMarker = __vihaco_fields_PPVM::__markers::cpu; -type CircuitMarker = __vihaco_fields_PPVM::__markers::circuit; +type Program = LocalModule; // execution stepping -impl Step> for PPVM +impl Step for PPVM where - Self: BuildMessage>, - >::Child: Execute, + Self: BuildMessage, + ExecutionState: Execute, { - fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { - let message = >>::build_message( + fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { + let message = >::build_message( self, instruction, module, )?; - >::get_mut(self).execute(instruction, message) + self.execution.execute(instruction, message) } } // CPU stepping -impl Step> for PPVM +impl Step for PPVM where - Self: BuildMessage>, - >::Child: Execute, - ExecutionState: - vihaco::Apply<<>::Child as Execute>::Effect>, + Self: BuildMessage, + vihaco_cpu::CPU: Execute, + ExecutionState: vihaco::Apply<>::Effect>, { - fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { - let message = >>::build_message( + fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { + let message = >::build_message( self, instruction, module, )?; - let effect = - >::get_mut(self).execute(instruction, message)?; + let effect = self.cpu.execute(instruction, message)?; vihaco::Apply::apply(&mut self.execution, effect); Ok(()) } } // handling circuit effects -impl vihaco::Apply> for PPVM { +impl vihaco::Apply> for PPVM { fn apply(&mut self, effects: Effects) { for effect in effects { match effect { @@ -106,635 +99,48 @@ impl vihaco::Apply> for PPVM } } -impl vihaco::Apply<()> for PPVM { +impl vihaco::Apply<()> for PPVM { fn apply(&mut self, _effect: ()) {} } // Circuit stepping -impl Step> for PPVM +impl Step for PPVM where - Self: BuildMessage>, - >::Child: Execute, - Self: vihaco::Apply<<>::Child as Execute>::Effect>, + Self: BuildMessage, + Circuit: Execute, + Self: vihaco::Apply<>::Effect>, { - fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { - let message = >>::build_message( + fn step(&mut self, instruction: &I, module: &Program) -> eyre::Result<()> { + let message = >::build_message( self, instruction, module, )?; - let effect = - >::get_mut(self).execute(instruction, message)?; + let effect = >::get_mut(self) + .execute(instruction, message)?; vihaco::Apply::apply(self, effect); Ok(()) } } -pub type TableauBackend64 = GeneralizedTableau>; - #[cfg(test)] #[allow(dead_code)] fn assert_dispatch_coverage( - m: &mut PPVM, - inst: &Instruction, - module: &Program, + m: &mut PPVM, + inst: &Instruction, + module: &Program, ) -> eyre::Result<()> { - vihaco::Dispatch::>::dispatch(m, inst, module) + vihaco::Dispatch::::dispatch(m, inst, module) } -// pub(crate) use self::ppvm as ppvm_module; - -// #[derive(Debug, Clone)] -// pub enum PPVMEffect { -// Step(StepOutcome), -// Stdout(StdoutEffect), -// Circuit(Box), -// Measurement(MeasurementEffect), -// Trace(TraceEffect), -// } - -// #[observe(vihaco::observer::stdio::StdoutEffect, effect = PPVMEffect)] -// impl PPVM { -// fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> eyre::Result> { -// Observe::::observe(&mut self.stdout, effect) -// } -// } - -// #[observe(CircuitEffect, effect = PPVMEffect)] -// impl PPVM { -// fn observe_circuit_effect( -// &mut self, -// effect: &CircuitEffect, -// ) -> eyre::Result> { -// Observe::::observe(&mut self.circuit, effect) -// } -// } - -// #[observe(MeasurementEffect, effect = PPVMEffect)] -// impl PPVM { -// fn observe_measurement_effect( -// &mut self, -// effect: &MeasurementEffect, -// ) -> eyre::Result> { -// Observe::::observe(&mut self.measurement_record, effect) -// } -// } - -// #[observe(TraceEffect, effect = PPVMEffect)] -// impl PPVM { -// fn observe_trace_effect(&mut self, effect: &TraceEffect) -> eyre::Result> { -// Observe::::observe(&mut self.trace_record, effect) -// } -// } - -// impl From for PPVMEffect { -// fn from(value: StdoutEffect) -> Self { -// Self::Stdout(value) -// } -// } - -// impl From for PPVMEffect { -// fn from(value: MeasurementEffect) -> Self { -// Self::Measurement(value) -// } -// } - -// impl From for PPVMEffect { -// fn from(value: TraceEffect) -> Self { -// Self::Trace(value) -// } -// } - -// impl From for PPVMEffect { -// fn from(value: CircuitOutcomeEffect) -> Self { -// match value { -// CircuitOutcomeEffect::Measurement(m) => Self::Measurement(m), -// CircuitOutcomeEffect::Trace(t) => Self::Trace(t), -// } -// } -// } - -// impl std::fmt::Display for PPVMInstruction { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// match self { -// PPVMInstruction::Cpu(inst) => inst.fmt(f), -// PPVMInstruction::Circuit(inst) => inst.fmt(f), -// } -// } -// } - -// impl PartialEq for PPVMInstruction { -// fn eq(&self, other: &Self) -> bool { -// match (self, other) { -// (PPVMInstruction::Cpu(a), PPVMInstruction::Cpu(b)) => a == b, -// (PPVMInstruction::Circuit(a), PPVMInstruction::Circuit(b)) => a == b, -// _ => false, -// } -// } -// } - -// impl From for PPVMInstruction { -// fn from(value: vihaco_cpu::RuntimeInstruction) -> Self { -// Self::Cpu(value) -// } -// } - -// impl From for PPVMInstruction { -// fn from(value: CircuitInstruction) -> Self { -// Self::Circuit(value) -// } -// } - -// impl PPVM { -// fn resolve_cpu(&mut self, inst: &vihaco_cpu::RuntimeInstruction) -> eyre::Result { -// match inst { -// vihaco_cpu::RuntimeInstruction::IndirectCall => { -// let function_id: u32 = self.cpu.stack_top()?.get_function_ref()?; -// let function = self.loader.get_function(function_id as usize)?; -// Ok(CPUMessage::FunctionInfo { -// arity: function.signature.params.len() as u32, -// start_address: function.start_address, -// }) -// } -// vihaco_cpu::RuntimeInstruction::Print => { -// let value = *self.cpu.stack_top()?; -// match value { -// vihaco::Value::String(addr) => { -// let string = self.loader.get_string(addr as usize)?.clone(); -// Ok(CPUMessage::Print(string)) -// } -// value => Ok(CPUMessage::Print(value.to_string())), -// } -// } -// _ => Ok(CPUMessage::None), -// } -// } - -// fn resolve_circuit(&mut self, inst: &CircuitInstruction) -> eyre::Result { -// use CircuitInstruction::*; -// match inst { -// X | Y | Z | H | S | SAdj | SqrtX | SqrtY | SqrtXAdj | SqrtYAdj | T | TAdj | Measure -// | Reset => { -// let q = self.pop_u64()?; -// Ok(CircuitMessage::Qubit(q)) -// } -// CNOT | CZ => { -// let q1 = self.pop_u64()?; -// let q0 = self.pop_u64()?; -// Ok(CircuitMessage::TwoQubit(q0, q1)) -// } -// RX | RY | RZ | Depolarize | Loss => { -// let theta = self.pop_f64()?; -// let q = self.pop_u64()?; -// Ok(CircuitMessage::QubitAndFloat(q, theta)) -// } -// Leakage => { -// // Push order: qubit, p0, p1. Pop reverse. -// let p1 = self.pop_f64()?; -// let p0 = self.pop_f64()?; -// let q = self.pop_u64()?; -// Ok(CircuitMessage::QubitAndTwoFloats(q, p0, p1)) -// } -// RXX | RYY | RZZ | Depolarize2 => { -// let theta = self.pop_f64()?; -// let q1 = self.pop_u64()?; -// let q0 = self.pop_u64()?; -// Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) -// } -// R => { -// let theta = self.pop_f64()?; -// let axis_angle = self.pop_f64()?; -// let q = self.pop_u64()?; -// Ok(CircuitMessage::QubitAndTwoFloats(q, axis_angle, theta)) -// } -// U3 => { -// let lam = self.pop_f64()?; -// let phi = self.pop_f64()?; -// let theta = self.pop_f64()?; -// let q = self.pop_u64()?; -// Ok(CircuitMessage::QubitU3(q, theta, phi, lam)) -// } - -// // TODO: pop actual float arrays? -// PauliError => { -// let pz = self.pop_f64()?; -// let py = self.pop_f64()?; -// let px = self.pop_f64()?; -// let q = self.pop_u64()?; -// Ok(CircuitMessage::QubitAndFloatArr3(q, [px, py, pz])) -// } -// CorrelatedLoss => { -// let p2 = self.pop_f64()?; -// let p1 = self.pop_f64()?; -// let p0 = self.pop_f64()?; -// let q1 = self.pop_u64()?; -// let q0 = self.pop_u64()?; -// Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) -// } -// TwoQubitPauliError => { -// let mut ps = [0.0; 15]; -// for p in ps.iter_mut().rev() { -// *p = self.pop_f64()?; -// } -// let q1 = self.pop_u64()?; -// let q0 = self.pop_u64()?; -// Ok(CircuitMessage::TwoQubitAndFloatArr15(q0, q1, ps)) -// } -// Trace => { -// let s = self.pop_string()?; -// Ok(CircuitMessage::PauliPatternStr(s)) -// } -// Truncate => Ok(CircuitMessage::None), -// } -// } - -// fn pop_u64(&mut self) -> eyre::Result { -// match self.cpu.stack_pop()? { -// vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into), -// v => Err(eyre::eyre!("Expected u64, got {:?}", v)), -// } -// } - -// fn pop_f64(&mut self) -> eyre::Result { -// match self.cpu.stack_pop()? { -// vihaco::Value::F64(v) => Ok(v), -// v => Err(eyre::eyre!("Expected f64 argument, got {:?}", v)), -// } -// } - -// /// Pop a `Value::String(addr)`, look up the addr in the module's string -// /// table, and return the owned string. Used by `Trace` to resolve its -// /// Pauli-pattern operand before the executor sees the message. -// fn pop_string(&mut self) -> eyre::Result { -// match self.cpu.stack_pop()? { -// vihaco::Value::String(addr) => Ok(self.loader.get_string(addr as usize)?.clone()), -// v => Err(eyre::eyre!("Expected string operand, got {:?}", v)), -// } -// } - -// pub fn init(&mut self) -> eyre::Result<()> { -// self.init_inner(None) -// } - -// /// Like [`PPVM::init`], but seed the circuit RNG deterministically so the -// /// run is reproducible. -// pub fn init_with_seed(&mut self, seed: u64) -> eyre::Result<()> { -// self.init_inner(Some(seed)) -// } - -// fn init_inner(&mut self, seed: Option) -> eyre::Result<()> { -// let info = &self.loader.module.extra; -// if info.n_qubits == 0 { -// return Err(eyre::eyre!("device circuit.n_qubits must be declared")); -// } -// self.circuit = match (info.backend, seed) { -// (BackendKind::Tableau, None) => Circuit::tableau(info)?, -// (BackendKind::Tableau, Some(seed)) => Circuit::tableau_with_seed(info, seed)?, -// (BackendKind::PauliSum, _) => { -// let terms = parse_observable_terms(info)?; -// Circuit::paulisum(info, &terms)? -// } -// (BackendKind::LossyPauliSum, _) => { -// let terms = parse_observable_terms(info)?; -// Circuit::lossy_paulisum(info, &terms)? -// } -// }; - -// // push entry frame -// self.cpu.push_frame(Frame { -// base: 0, -// span: (0, 0, 0), -// function: None, -// ret_pc: 0, -// }); - -// Ok(()) -// } - -// pub fn load( -// &mut self, -// module: &vihaco::module::LocalModule< -// PPVMInstruction, -// vihaco::Value, -// vihaco::Type, -// PPVMDeviceInfo, -// >, -// ) -> eyre::Result<()> { -// self.loader.module = module.clone(); -// Ok(()) -// } - -// pub fn step_once(&mut self) -> eyre::Result { -// let inst = self.loader.peek_instruction()?.clone(); -// let effects = self.execute_effects(inst)?; -// self.continue_effects(effects) -// } - -// /// Program counter: the index of the next instruction to execute. -// pub fn current_pc(&self) -> u32 { -// self.loader.pc() -// } - -// /// The next instruction to execute, or `None` once execution has run off -// /// the end of the code. Intended for debuggers/inspection. -// pub fn current_instruction(&self) -> Option { -// self.loader.peek_instruction().ok().cloned() -// } - -// /// Append one REPL command's lowered VM ops and run just that block against -// /// the persistent state, advancing the pc through it. NOTE: an out-of-range -// /// qubit index *panics* in the tableau rather than erroring, so callers must -// /// bounds-check qubit operands against `n_qubits` first. -// pub fn execute_single_instruction(&mut self, instrs: &[PPVMInstruction]) -> eyre::Result<()> { -// let start = self.loader.module.code.len() as u32; -// self.loader.module.code.extend_from_slice(instrs); -// *self.loader.pc_mut() = start; -// for _ in 0..instrs.len() { -// self.step_once()?; -// } -// Ok(()) -// } - -// /// Render the current circuit state (tableau / Pauli sum) for the REPL's -// /// `show` command. Delegates to the circuit's size-specific tableau. -// pub fn state_string(&self) -> String { -// self.circuit.state_string() -// } - -// /// Build a fresh, initialized `n_qubits`-qubit device with no code. The -// /// REPL's `device` command uses this to (re)create the machine. Errors if -// /// `n_qubits` is zero (a device must have at least one qubit). -// pub fn with_qubits(n_qubits: usize) -> eyre::Result { -// let mut machine = Self::default(); -// let mut module = vihaco::module::LocalModule::< -// PPVMInstruction, -// Value, -// vihaco::Type, -// PPVMDeviceInfo, -// >::default(); -// module.extra.n_qubits = n_qubits; -// machine.load(&module)?; -// machine.init()?; -// Ok(machine) -// } - -// /// Lower a single circuit instruction — qubit operands first, then float -// /// params, per the push-in-order / pop-in-reverse convention — and execute -// /// it against the persistent state. Qubit indices are bounds-checked against -// /// the device size first, because the tableau panics (rather than erroring) -// /// on an out-of-range qubit. -// pub fn apply_circuit_instruction( -// &mut self, -// inst: CircuitInstruction, -// qubits: &[usize], -// params: &[f64], -// ) -> eyre::Result<()> { -// let n_qubits = self.loader.module.extra.n_qubits; -// for &q in qubits { -// if q >= n_qubits { -// eyre::bail!("qubit {q} out of range for {n_qubits}-qubit device"); -// } -// } - -// let mut instrs = Vec::with_capacity(qubits.len() + params.len() + 1); -// for &q in qubits { -// instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( -// Type::U64, -// Value::U64(q as u64), -// ))); -// } -// for &p in params { -// instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( -// Type::F64, -// Value::F64(p), -// ))); -// } -// instrs.push(PPVMInstruction::Circuit(inst)); - -// // Run the appended block, then roll back pc, code, and operand stack (on -// // every path, including error) so the injected op is transparent to a -// // paused program — only its tableau/measurement effects persist. -// let saved_pc = self.loader.pc(); -// let saved_len = self.loader.module.code.len(); -// let saved_stack = self.cpu.stack_len(); -// let result = self.execute_single_instruction(&instrs); -// self.loader.module.code.truncate(saved_len); -// *self.loader.pc_mut() = saved_pc; -// self.cpu.stack_mut().truncate(saved_stack); -// result -// } - -// fn execute_effects(&mut self, inst: PPVMInstruction) -> eyre::Result> { -// log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); -// match inst { -// PPVMInstruction::Cpu(cpu_inst) => { -// let msg = self.resolve_cpu(&cpu_inst)?; -// self.cpu.set_current_pc(self.loader.pc()); -// let stdout_effect = match (&cpu_inst, &msg) { -// ( -// vihaco_cpu::RuntimeInstruction::Print, -// vihaco_cpu::CPUMessage::Print(text), -// ) => Some(PPVMEffect::Stdout(StdoutEffect(text.clone()))), -// _ => None, -// }; -// let outcome = vihaco::expect_exactly_one_effect( -// vihaco::GeneratedComponent::execute_generated(&mut self.cpu, cpu_inst, msg)?, -// )?; -// // Advance past a cpu::cpu.breakpoint as well, so the debugger that paused -// // on it doesn't re-hit the same instruction on the next step. -// if matches!(outcome, StepOutcome::Continue | StepOutcome::Breakpoint) { -// if let Some(target) = self.cpu.take_pending_pc() { -// *self.loader.pc_mut() = target; -// } else { -// *self.loader.pc_mut() += 1; -// } -// } -// let mut effects = Effects::one(PPVMEffect::Step(outcome)); -// if let Some(stdout_effect) = stdout_effect { -// effects = effects.append(stdout_effect); -// } -// Ok(effects) -// } -// PPVMInstruction::Circuit(inst) => { -// let msg = self.resolve_circuit(&inst)?; -// let circuit_effects = ::execute_generated( -// &mut self.circuit, -// inst, -// msg, -// )?; -// *self.loader.pc_mut() += 1; -// let mut effects = Effects::one(PPVMEffect::Step(StepOutcome::Continue)); -// for outcome in circuit_effects { -// effects = effects.append(PPVMEffect::from(outcome)); -// } -// Ok(effects) -// } -// } -// } - -// fn continue_effects(&mut self, effects: Effects) -> eyre::Result { -// let mut step_outcome = None; -// for effect in effects { -// match effect { -// PPVMEffect::Step(outcome) => { -// if step_outcome.replace(outcome).is_some() { -// return Err(eyre::eyre!( -// "expected exactly one PPVM step effect, got multiple" -// )); -// } -// } -// effect => self.continue_observer_effect(effect)?, -// } -// } - -// step_outcome.ok_or_else(|| eyre::eyre!("expected exactly one PPVM step effect, got 0")) -// } - -// fn continue_observer_effect(&mut self, effect: PPVMEffect) -> eyre::Result<()> { -// match effect { -// PPVMEffect::Stdout(effect) => { -// let follow_ups = Observe::::observe(self, &effect)?; -// self.continue_observer_effects(follow_ups) -// } -// PPVMEffect::Circuit(effect) => { -// let follow_ups = Observe::::observe(self, &effect)?; -// self.continue_observer_effects(follow_ups) -// } -// PPVMEffect::Measurement(effect) => { -// let follow_ups = Observe::::observe(self, &effect)?; -// // NOTE: push measurement outcomes onto the stack as u32 (0/1/2, where 2 = Lost). -// for outcome in effect.measurement_results { -// let m = Value::U32(outcome as u32); -// self.cpu.stack_push(m); -// } -// self.continue_observer_effects(follow_ups) -// } -// PPVMEffect::Trace(effect) => { -// let value = effect.value; -// let follow_ups = Observe::::observe(self, &effect)?; -// // Mirror the measurement wiring: append to the trace record -// // (via the observer above) AND push the value onto the CPU -// // stack so user bytecode can consume it. Plan Task 7. -// self.cpu.stack_push(Value::F64(value)); -// self.continue_observer_effects(follow_ups) -// } -// PPVMEffect::Step(_) => Err(eyre::eyre!( -// "unexpected Step effect while continuing PPVM observer follow-ups" -// )), -// } -// } - -// fn continue_observer_effects(&mut self, effects: Effects) -> eyre::Result<()> { -// for effect in effects { -// self.continue_observer_effect(effect)?; -// } -// Ok(()) -// } - -// pub fn run(&mut self) -> eyre::Result { -// self.run_with_seed(None) -// } - -// /// Like [`PPVM::run`], but seed the circuit RNG deterministically when -// /// `seed` is `Some`, making the run reproducible. -// pub fn run_with_seed(&mut self, seed: Option) -> eyre::Result { -// match seed { -// Some(seed) => self.init_with_seed(seed)?, -// None => self.init()?, -// } - -// loop { -// // Breakpoints only pause the interactive debugger; a batch run -// // skips straight past them. -// match self.step_once()? { -// StepOutcome::Continue | StepOutcome::Breakpoint => continue, -// action => return Ok(action), -// } -// } -// } - -// pub fn stdout(&self) -> &[u8] { -// self.stdout.output() -// } - -// pub fn measurement_record(&self) -> Vec { -// self.measurement_record.record.clone() -// } - -// /// Per-trace values collected by `Trace` instructions during the run. -// /// Parallel to [`PPVM::measurement_record`]: one f64 per `Trace` executed, -// /// in execution order. -// pub fn trace_record(&self) -> Vec { -// self.trace_record.record.clone() -// } - -// pub fn load_program(&mut self, program: &str) -> eyre::Result<()> { -// let module = crate::compile_program(program)?; -// self.load(&module)?; -// Ok(()) -// } - -// /// Load from a file, auto-detecting the format: if it starts with the PPVM -// /// magic it is loaded as `.ssb` bytecode, otherwise it is parsed as `.sst` -// /// source text. A magic match commits to the bytecode path — a corrupt -// /// `.ssb` errors rather than silently falling back to the text parser. -// pub fn load_file(&mut self, path: &str) -> eyre::Result<()> { -// let bytes = std::fs::read(path)?; -// if crate::bytecode::is_bytecode(&bytes) { -// self.load_bytecode(&bytes) -// } else { -// self.load_program(std::str::from_utf8(&bytes)?) -// } -// } - -// /// Load a module from an in-memory `.ssb` byte stream. -// pub fn load_bytecode(&mut self, bytes: &[u8]) -> eyre::Result<()> { -// let module = crate::bytecode::module_from_bytes(bytes)?; -// self.load(&module) -// } - -// /// Read a `.ssb` file and load the module it contains. -// pub fn load_bytecode_file(&mut self, path: &str) -> eyre::Result<()> { -// let bytes = std::fs::read(path)?; -// self.load_bytecode(&bytes) -// } - -// pub fn run_program(&mut self, program: &str) -> eyre::Result<()> { -// self.load_program(program)?; -// self.run()?; -// Ok(()) -// } - -// pub fn run_file(&mut self, path: &str) -> eyre::Result<()> { -// self.load_file(path)?; -// self.run()?; -// Ok(()) -// } -// } - -// impl vihaco::Reset for PPVM { -// fn reset(&mut self) { -// self.cpu.reset(); -// self.circuit.reset(); -// self.loader.pc = 0; -// self.measurement_record.record.clear(); -// self.trace_record.record.clear(); -// } -// } - -// /// Parse the `device circuit.observable` header into Pauli-sum terms ready to -// /// seed a `PauliSum` / `LossyPauliSum` state. Single-Pauli observables from -// /// Phase 2 keep working as the degenerate one-term case; multi-term sums like -// /// `"1.0*ZZ + 0.5*XX"` are handled by [`parse_pauli_sum_terms`]. -// fn parse_observable_terms(info: &PPVMDeviceInfo) -> Result> { -// let observable = info.observable.as_deref().ok_or_else(|| { -// eyre!( -// "the {:?} backend requires `device circuit.observable` to be set", -// info.backend -// ) -// })?; -// crate::observable::parse_pauli_sum_terms(observable, info.n_qubits) -// } +impl vihaco::Reset for PPVM { + fn reset(&mut self) { + self.cpu.reset(); + self.circuit.reset(); + self.measurement_record.record.clear(); + self.trace_record.record.clear(); + } +} // #[cfg(test)] // mod tests { From 86ba2076791688458ad3fe938e2d592fbce9a7b9 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 13:36:39 +0100 Subject: [PATCH 13/36] Add constructors --- crates/ppvm-vihaco/src/component.rs | 181 ++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index c3d838fde..8f0fdbedd 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -462,6 +462,60 @@ impl TableauBuckets { Self::Bits2048(c) => vihaco::Reset::reset(c), } } + + pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Result { + if n_qubits <= 64 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits64(CircuitExecutor { backend: tab })) + } else if n_qubits <= 128 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits128(CircuitExecutor { backend: tab })) + } else if n_qubits <= 256 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits256(CircuitExecutor { backend: tab })) + } else if n_qubits <= 512 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits512(CircuitExecutor { backend: tab })) + } else if n_qubits <= 1024 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits1024(CircuitExecutor { backend: tab })) + } else if n_qubits <= MAX_QUBITS { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits2048(CircuitExecutor { backend: tab })) + } else { + Err(eyre!( + "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" + )) + } + } + + /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a + /// shot is reproducible. + pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Result { + macro_rules! seeded { + ($variant:ident) => {{ + let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); + Ok(Self::$variant(CircuitExecutor { backend: tab })) + }}; + } + if n_qubits <= 64 { + seeded!(Bits64) + } else if n_qubits <= 128 { + seeded!(Bits128) + } else if n_qubits <= 256 { + seeded!(Bits256) + } else if n_qubits <= 512 { + seeded!(Bits512) + } else if n_qubits <= 1024 { + seeded!(Bits1024) + } else if n_qubits <= MAX_QUBITS { + seeded!(Bits2048) + } else { + Err(eyre!( + "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" + )) + } + } } pub(crate) enum PauliSumBuckets { @@ -495,6 +549,47 @@ impl PauliSumBuckets { Self::Bits2048(c) => vihaco::Reset::reset(c), } } + + /// Build a PauliSum-backed circuit, seeding the state with every term: + /// `for (word, coef) in terms { state += (word, coef); }`. Words must + /// already be validated against `info.n_qubits` by the caller. + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let mut state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } + let initial_state = state.clone(); + let psum = PauliSumBackend::> { + state, + initial_state, + }; + Ok(Self::$variant(CircuitExecutor { backend: psum })) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= MAX_QUBITS { + build!(Bits2048, 256) + } else { + Err(eyre!( + "cannot simulate {} qubits: maximum is {MAX_QUBITS}", + info.n_qubits + )) + } + } } pub(crate) enum LossyPauliSumBuckets { @@ -528,6 +623,44 @@ impl LossyPauliSumBuckets { Self::Bits2048(c) => vihaco::Reset::reset(c), } } + + fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let mut state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } + let initial_state = state.clone(); + let psum = PauliSumBackend::> { + state, + initial_state, + }; + Ok(Self::$variant(CircuitExecutor { backend: psum })) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= MAX_QUBITS { + build!(Bits2048, 256) + } else { + Err(eyre!( + "cannot simulate {} qubits: maximum is {MAX_QUBITS}", + info.n_qubits + )) + } + } } pub(crate) enum CircuitKind { @@ -598,3 +731,51 @@ impl vihaco::Reset for TableauCircuit { self.inner.reset() } } + +impl Circuit { + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and + /// `coefficient_threshold` from `info`; no observable required. + pub fn tableau(info: &PPVMDeviceInfo) -> Result { + let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; + Ok(Self { + inner: CircuitKind::Tableau(tab), + }) + } + + /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { + let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; + Ok(Self { + inner: CircuitKind::Tableau(tab), + }) + } + + /// Build a PauliSum-backed circuit, seeding the state with every term in + /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the + /// caller is responsible for having parsed/validated the words against + /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). + pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + let psum = PauliSumBuckets::new(info, terms)?; + Ok(Self { + inner: CircuitKind::PauliSum(psum), + }) + } + + /// Build a LossyPauliSum-backed circuit. Same contract as + /// [`Circuit::paulisum`]. + pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + let psum = LossyPauliSumBuckets::new(info, terms)?; + Ok(Self { + inner: CircuitKind::LossyPauliSum(psum), + }) + } +} + +impl Default for Circuit { + fn default() -> Self { + // Default backend is Tableau with 0 qubits, which always fits the + // smallest bucket, so construction here is infallible. + Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") + } +} From 6ca984014cd12cb0cd87d93c37059c713ac5ca67 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 13:45:12 +0100 Subject: [PATCH 14/36] Add some comments --- crates/ppvm-vihaco/src/component.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 8f0fdbedd..2ae3d4842 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -178,6 +178,7 @@ where } } +/// Generic backend component - use this if you need one concrete variant in your machine #[component] #[instructions { circuit }] pub struct CircuitExecutor { @@ -679,6 +680,7 @@ impl CircuitKind { } } +/// circuit component that selects qubit size from a runtime value and can do all backends #[component] #[instructions { circuit }] pub struct Circuit { @@ -699,7 +701,7 @@ impl Execute for Circuit { } } -// Tableau-only component +/// Tableau-only component with runtime configured size #[component] #[instructions { circuit }] pub struct TableauCircuit { From c815f6695ae4d5ca7b4d4bd3eb5a83bcfa80a836 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 14:00:10 +0100 Subject: [PATCH 15/36] Add constructor for TableauCircuit --- crates/ppvm-vihaco/src/component.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 2ae3d4842..ddea51a58 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -781,3 +781,25 @@ impl Default for Circuit { Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") } } + +impl TableauCircuit { + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and + /// `coefficient_threshold` from `info`; no observable required. + pub fn new(info: &PPVMDeviceInfo) -> Result { + let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; + Ok(Self { inner: tab }) + } + + /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn new_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { + let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; + Ok(Self { inner: tab }) + } +} + +impl Default for TableauCircuit { + fn default() -> Self { + Self::new(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") + } +} From 2777670e9c7a7bb0b40a191780eacbf8b034c36f Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 14:24:04 +0100 Subject: [PATCH 16/36] Split up component.rs --- crates/ppvm-vihaco/src/component.rs | 805 ------------------- crates/ppvm-vihaco/src/component/backend.rs | 153 ++++ crates/ppvm-vihaco/src/component/buckets.rs | 275 +++++++ crates/ppvm-vihaco/src/component/messages.rs | 4 + crates/ppvm-vihaco/src/component/mod.rs | 168 ++++ crates/ppvm-vihaco/src/component/ops.rs | 253 ++++++ 6 files changed, 853 insertions(+), 805 deletions(-) delete mode 100644 crates/ppvm-vihaco/src/component.rs create mode 100644 crates/ppvm-vihaco/src/component/backend.rs create mode 100644 crates/ppvm-vihaco/src/component/buckets.rs create mode 100644 crates/ppvm-vihaco/src/component/messages.rs create mode 100644 crates/ppvm-vihaco/src/component/mod.rs create mode 100644 crates/ppvm-vihaco/src/component/ops.rs diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs deleted file mode 100644 index ddea51a58..000000000 --- a/crates/ppvm-vihaco/src/component.rs +++ /dev/null @@ -1,805 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The PPVM Authors -// SPDX-License-Identifier: Apache-2.0 - -use crate::device_info::PPVMDeviceInfo; -use crate::measurements::{ - CircuitOutcomeEffect, MeasurementEffect, MeasurementOutcome, TraceEffect, -}; -use bitvec::view::BitView; -use bnum::types::{U256, U512, U1024, U2048}; -use eyre::{Result, eyre}; -use num::PrimInt; -use num::complex::Complex64; -use ppvm_pauli_sum::config::fx64hash::Byte8F64; -use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; -use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; -use ppvm_tableau::prelude::*; -use vihaco::{Apply, Component, Effects, Execute, PopFromStack, component, dispatch, observe}; -use vihaco_circuit_isa::circuit; - -/// Largest qubit count any backend can simulate. The widest size bucket is -/// backed by 2048-bit integers (`U2048` / `[u8; 256]`), so every constructor -/// rejects `n_qubits > MAX_QUBITS` rather than panicking. -pub const MAX_QUBITS: usize = 2048; - -/// Truncation strategy used by every `PauliSum` / `LossyPauliSum` size bucket. -/// Coefficient-threshold pruning is always on; the Pauli-weight cap is set per -/// run from the header (defaults to `usize::MAX` = no cap). -type PauliSumStrategy = CombinedStrategy; - -/// `PauliSum`'s `T` for the lossless backend: `[u8; N]` storage, fx hash, -/// f64 coefficients, the strategy above. -type PauliSumConfig = ByteFxHashF64; - -/// Same as `PauliSumConfig` but with `LossyPauliWord` as the word type, so the -/// loss-channel methods are dispatchable on the resulting `PauliSum`. -/// `LossyPauliWord`'s second type parameter (hasher) defaults to -/// `fxhash::FxBuildHasher`, matching `ByteFxHashF64`'s internal hasher. -type LossyPauliSumConfig = - ByteFxHashF64>; - -/// Build a `PauliSumStrategy` value from a `PPVMDeviceInfo`. Pulled out so the -/// six size-bucket constructors don't each repeat the strategy spelling. -fn paulisum_strategy(info: &PPVMDeviceInfo) -> PauliSumStrategy { - CombinedStrategy( - CoefficientThreshold(info.coefficient_threshold), - MaxPauliWeight(info.max_pauli_weight.unwrap_or(usize::MAX)), - ) -} - -macro_rules! batch_for { - ($tab:expr, $method:ident, $addrs:expr) => { - for addr in $addrs { $tab.$method(*addr); } - }; - ($tab:expr, $method:ident, $addrs:expr, $($arg:expr),+) => { - for addr in $addrs { $tab.$method(*addr, $($arg),+); } - }; -} - -/// Two-qubit sibling of [`batch_for!`]: drives a method that takes two qubit -/// addresses (plus optional extra args) over a slice of `(usize, usize)` pairs. -macro_rules! batch_pairs_for { - ($state:expr, $method:ident, $pairs:expr) => { - for &(a, b) in $pairs { $state.$method(a, b); } - }; - ($state:expr, $method:ident, $pairs:expr, $($arg:expr),+) => { - for &(a, b) in $pairs { $state.$method(a, b, $($arg),+); } - }; -} - -/// Trait defining all methods required to implement the backend of the VM component -pub trait CircuitBackend { - type Config: Config; - type State: Clifford - + CliffordExtensions - + Reset - + TGate - + U3Gate - + RotationOne - + RotXY - + RotationTwo - + Depolarizing - + Depolarizing2 - + PauliError - + TwoQubitPauliError - + LossChannel - + CorrelatedLossChannel; - - fn state(&mut self) -> &mut Self::State; - - // NOTE: methods that differ between backends get dedicated methods - - fn measure(&mut self, _addr0: usize) -> eyre::Result { - Err(eyre!("measure is not supported on this backend")) - } - - fn leakage(&mut self, _addr0: usize, p0: f64, p1: f64) -> eyre::Result<()> { - Err(eyre!("leakage is not supported on this backend")) - } - - fn truncate(&mut self); - - fn reset(&mut self); -} - -impl CircuitBackend for GeneralizedTableau -where - T: Config, - <::Storage as BitView>::Store: PrimInt, - I: TableauIndex + Send + Sync + std::fmt::Debug, - C: SparseVector + std::fmt::Debug, -{ - type Config = T; - type State = GeneralizedTableau; - - fn state(&mut self) -> &mut Self::State { - self - } - - fn measure(&mut self, q: usize) -> eyre::Result { - let m = LossyMeasure::measure(self, q); - let e = match m { - Some(b) => { - if b { - MeasurementOutcome::One - } else { - MeasurementOutcome::Zero - } - } - None => MeasurementOutcome::Lost, - }; - Ok(e) - } - - fn truncate(&mut self) {} - - fn reset(&mut self) { - self.reset_all(); - } - - fn leakage(&mut self, _addr0: usize, p0: f64, p1: f64) -> eyre::Result<()> { - self.leakage_channel(_addr0, p0, p1); - Ok(()) - } -} - -/// PauliSum that also stores the initial observable so we can reset the component -pub struct PauliSumBackend> { - pub state: PauliSum, - initial_state: PauliSum, -} - -impl> PauliSumBackend { - pub fn new(observable: PauliSum) -> Self { - Self { - state: observable.clone(), - initial_state: observable.clone(), - } - } -} - -impl CircuitBackend for PauliSumBackend -where - T: Config, -{ - type Config = T; - type State = PauliSum; - - fn state(&mut self) -> &mut Self::State { - &mut self.state - } - - fn reset(&mut self) { - self.state = self.initial_state.clone(); - } - - fn truncate(&mut self) { - self.state.truncate(); - } -} - -/// Generic backend component - use this if you need one concrete variant in your machine -#[component] -#[instructions { circuit }] -pub struct CircuitExecutor { - pub backend: B, -} - -// CircuitOp trait: define how an instruction executes on a generic backend -// This is used to avoid having huge where constraints on the enums later -// - without this trait, we'd have to constrain every enum variant to all Execute impls -// TODO: re-evaluate whether this is needed once we refactor and try to get rid of enums -pub trait CircuitOp { - type Message; - type Effect; - - fn apply( - &self, - backend: &mut B, - msg: Self::Message, - ) -> eyre::Result; -} - -macro_rules! single_qubit_execute { - ($leaf:ident, $method:ident) => { - impl CircuitOp for circuit::$leaf { - type Effect = (); - type Message = PopFromStack<(u64,)>; - - fn apply( - &self, - backend: &mut B, - msg: Self::Message, - ) -> eyre::Result<()> { - backend.state().$method(msg.0.0 as usize); - Ok(()) - } - } - }; -} - -macro_rules! two_qubit_execute { - ($leaf:ident, $method:ident) => { - impl CircuitOp for circuit::$leaf { - type Effect = (); - type Message = PopFromStack<(u64, u64)>; - - fn apply( - &self, - backend: &mut B, - msg: Self::Message, - ) -> eyre::Result<()> { - backend.state().$method(msg.0.0 as usize, msg.0.1 as usize); - Ok(()) - } - } - }; -} - -macro_rules! single_qubit_rotation_execute { - ($leaf:ident, $method:ident) => { - impl CircuitOp for circuit::$leaf { - type Effect = (); - type Message = PopFromStack<(u64, f64)>; - - fn apply( - &self, - backend: &mut B, - msg: Self::Message, - ) -> eyre::Result<()> { - backend.state().$method(msg.0.0 as usize, msg.0.1); - Ok(()) - } - } - }; -} - -macro_rules! two_qubit_rotation_execute { - ($leaf:ident, $method:ident) => { - impl CircuitOp for circuit::$leaf { - type Effect = (); - type Message = PopFromStack<(u64, u64, f64)>; - - fn apply( - &self, - backend: &mut B, - msg: Self::Message, - ) -> eyre::Result<()> { - backend - .state() - .$method(msg.0.0 as usize, msg.0.1 as usize, msg.0.2); - Ok(()) - } - } - }; -} - -single_qubit_execute!(X, x); -single_qubit_execute!(Y, y); -single_qubit_execute!(Z, z); -single_qubit_execute!(T, t); -single_qubit_execute!(S, s); -single_qubit_execute!(TAdj, t_dag); -single_qubit_execute!(SAdj, s_dag); -single_qubit_execute!(SqrtX, sqrt_x); -single_qubit_execute!(H, h); -single_qubit_execute!(SqrtXAdj, sqrt_x_dag); -single_qubit_execute!(SqrtY, sqrt_y); -single_qubit_execute!(SqrtYAdj, sqrt_y_dag); - -two_qubit_execute!(CNOT, cnot); -two_qubit_execute!(CZ, cz); - -single_qubit_rotation_execute!(RX, rx); -single_qubit_rotation_execute!(RY, ry); -single_qubit_rotation_execute!(RZ, rz); - -two_qubit_rotation_execute!(RXX, rxx); -two_qubit_rotation_execute!(RYY, ryy); -two_qubit_rotation_execute!(RZZ, rzz); - -single_qubit_execute!(Reset, reset); -single_qubit_rotation_execute!(Depolarize, depolarize1); -single_qubit_rotation_execute!(Loss, loss_channel); -two_qubit_rotation_execute!(Depolarize2, depolarize2); - -// TODO: batched gates; need custom messages - -impl CircuitOp for circuit::U3 { - type Effect = (); - type Message = PopFromStack<(u64, f64, f64, f64)>; - - fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { - backend - .state() - .u3(msg.0.0 as usize, msg.0.1, msg.0.2, msg.0.3); - Ok(()) - } -} - -impl CircuitOp for circuit::R { - type Effect = (); - type Message = PopFromStack<(u64, f64, f64)>; - - fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { - backend.state().r(msg.0.0 as usize, msg.0.1, msg.0.2); - Ok(()) - } -} - -impl CircuitOp for circuit::Leakage { - type Effect = (); - type Message = PopFromStack<(u64, f64, f64)>; - - fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { - backend.leakage(msg.0.0 as usize, msg.0.1, msg.0.2)?; - Ok(()) - } -} - -impl CircuitOp for circuit::PauliError { - type Effect = (); - type Message = PopFromStack<(u64, f64, f64, f64)>; - - fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { - backend - .state() - .pauli_error(msg.0.0 as usize, [msg.0.1, msg.0.2, msg.0.3]); - Ok(()) - } -} - -impl CircuitOp for circuit::TwoQubitPauliError { - type Effect = (); - type Message = PopFromStack<(u64, u64, f64, f64)>; - - fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { - // TODO: probably needs a custom message since we can't pop float arrays - unimplemented!("TODO: needs a custom message") - } -} - -impl CircuitOp for circuit::CorrelatedLoss { - type Effect = (); - type Message = PopFromStack<(u64, u64, f64, f64, f64)>; - - fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { - backend.state().correlated_loss_channel( - msg.0.0 as usize, - msg.0.1 as usize, - [msg.0.2, msg.0.3, msg.0.4], - ); - Ok(()) - } -} - -impl CircuitOp for circuit::Measure { - type Effect = Effects; - type Message = PopFromStack<(u64,)>; - - fn apply( - &self, - backend: &mut B, - msg: Self::Message, - ) -> eyre::Result { - let outcome = backend.measure(msg.0.0 as usize)?; - Ok(Effects::one(CircuitOutcomeEffect::Measurement( - MeasurementEffect { - measurement_results: smallvec::smallvec![outcome], - }, - ))) - } -} - -impl CircuitOp for circuit::Truncate { - type Effect = (); - type Message = vihaco::NoMessage; - - fn apply(&self, backend: &mut B, _: Self::Message) -> eyre::Result<()> { - backend.truncate(); - Ok(()) - } -} - -impl CircuitOp for circuit::Trace { - type Effect = (); - type Message = vihaco::NoMessage; - - fn apply(&self, backend: &mut B, _: Self::Message) -> eyre::Result<()> { - unimplemented!("TODO: needs a custom message"); - } -} - -impl vihaco::Reset for CircuitExecutor { - fn reset(&mut self) { - self.backend.reset(); - } -} - -// The single execute impl for all CircuitOps -impl Execute for CircuitExecutor { - type Effect = I::Effect; - type Message = I::Message; - type Error = eyre::Report; - - fn execute( - &mut self, - instruction: &I, - message: Self::Message, - ) -> Result { - I::apply(instruction, &mut self.backend, message) - } -} - -// TODO: enum dispatch can go once we only set the index type in tableau #204 -pub(crate) enum TableauBuckets { - Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>>), - Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>>), - Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>>), - Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>>), - Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>>), - Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>>), -} - -impl TableauBuckets { - fn apply(&mut self, op: &I, msg: I::Message) -> eyre::Result { - match self { - Self::Bits64(c) => op.apply(&mut c.backend, msg), - Self::Bits128(c) => op.apply(&mut c.backend, msg), - Self::Bits256(c) => op.apply(&mut c.backend, msg), - Self::Bits512(c) => op.apply(&mut c.backend, msg), - Self::Bits1024(c) => op.apply(&mut c.backend, msg), - Self::Bits2048(c) => op.apply(&mut c.backend, msg), - } - } - - fn reset(&mut self) { - match self { - Self::Bits64(c) => vihaco::Reset::reset(c), - Self::Bits128(c) => vihaco::Reset::reset(c), - Self::Bits256(c) => vihaco::Reset::reset(c), - Self::Bits512(c) => vihaco::Reset::reset(c), - Self::Bits1024(c) => vihaco::Reset::reset(c), - Self::Bits2048(c) => vihaco::Reset::reset(c), - } - } - - pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Result { - if n_qubits <= 64 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits64(CircuitExecutor { backend: tab })) - } else if n_qubits <= 128 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits128(CircuitExecutor { backend: tab })) - } else if n_qubits <= 256 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits256(CircuitExecutor { backend: tab })) - } else if n_qubits <= 512 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits512(CircuitExecutor { backend: tab })) - } else if n_qubits <= 1024 { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits1024(CircuitExecutor { backend: tab })) - } else if n_qubits <= MAX_QUBITS { - let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Ok(Self::Bits2048(CircuitExecutor { backend: tab })) - } else { - Err(eyre!( - "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" - )) - } - } - - /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a - /// shot is reproducible. - pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Result { - macro_rules! seeded { - ($variant:ident) => {{ - let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); - Ok(Self::$variant(CircuitExecutor { backend: tab })) - }}; - } - if n_qubits <= 64 { - seeded!(Bits64) - } else if n_qubits <= 128 { - seeded!(Bits128) - } else if n_qubits <= 256 { - seeded!(Bits256) - } else if n_qubits <= 512 { - seeded!(Bits512) - } else if n_qubits <= 1024 { - seeded!(Bits1024) - } else if n_qubits <= MAX_QUBITS { - seeded!(Bits2048) - } else { - Err(eyre!( - "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" - )) - } - } -} - -pub(crate) enum PauliSumBuckets { - Bits64(CircuitExecutor>>), - Bits128(CircuitExecutor>>), - Bits256(CircuitExecutor>>), - Bits512(CircuitExecutor>>), - Bits1024(CircuitExecutor>>), - Bits2048(CircuitExecutor>>), -} - -impl PauliSumBuckets { - fn apply(&mut self, op: &I, msg: I::Message) -> eyre::Result { - match self { - Self::Bits64(c) => op.apply(&mut c.backend, msg), - Self::Bits128(c) => op.apply(&mut c.backend, msg), - Self::Bits256(c) => op.apply(&mut c.backend, msg), - Self::Bits512(c) => op.apply(&mut c.backend, msg), - Self::Bits1024(c) => op.apply(&mut c.backend, msg), - Self::Bits2048(c) => op.apply(&mut c.backend, msg), - } - } - - fn reset(&mut self) { - match self { - Self::Bits64(c) => vihaco::Reset::reset(c), - Self::Bits128(c) => vihaco::Reset::reset(c), - Self::Bits256(c) => vihaco::Reset::reset(c), - Self::Bits512(c) => vihaco::Reset::reset(c), - Self::Bits1024(c) => vihaco::Reset::reset(c), - Self::Bits2048(c) => vihaco::Reset::reset(c), - } - } - - /// Build a PauliSum-backed circuit, seeding the state with every term: - /// `for (word, coef) in terms { state += (word, coef); }`. Words must - /// already be validated against `info.n_qubits` by the caller. - pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - macro_rules! build { - ($variant:ident, $N:literal) => {{ - let mut state = PauliSum::>::builder() - .n_qubits(info.n_qubits) - .strategy(paulisum_strategy(info)) - .build(); - for (word, coef) in terms { - state += (word.as_str(), *coef); - } - let initial_state = state.clone(); - let psum = PauliSumBackend::> { - state, - initial_state, - }; - Ok(Self::$variant(CircuitExecutor { backend: psum })) - }}; - } - if info.n_qubits <= 64 { - build!(Bits64, 8) - } else if info.n_qubits <= 128 { - build!(Bits128, 16) - } else if info.n_qubits <= 256 { - build!(Bits256, 32) - } else if info.n_qubits <= 512 { - build!(Bits512, 64) - } else if info.n_qubits <= 1024 { - build!(Bits1024, 128) - } else if info.n_qubits <= MAX_QUBITS { - build!(Bits2048, 256) - } else { - Err(eyre!( - "cannot simulate {} qubits: maximum is {MAX_QUBITS}", - info.n_qubits - )) - } - } -} - -pub(crate) enum LossyPauliSumBuckets { - Bits64(CircuitExecutor>>), - Bits128(CircuitExecutor>>), - Bits256(CircuitExecutor>>), - Bits512(CircuitExecutor>>), - Bits1024(CircuitExecutor>>), - Bits2048(CircuitExecutor>>), -} - -impl LossyPauliSumBuckets { - fn apply(&mut self, op: &I, msg: I::Message) -> eyre::Result { - match self { - Self::Bits64(c) => op.apply(&mut c.backend, msg), - Self::Bits128(c) => op.apply(&mut c.backend, msg), - Self::Bits256(c) => op.apply(&mut c.backend, msg), - Self::Bits512(c) => op.apply(&mut c.backend, msg), - Self::Bits1024(c) => op.apply(&mut c.backend, msg), - Self::Bits2048(c) => op.apply(&mut c.backend, msg), - } - } - - fn reset(&mut self) { - match self { - Self::Bits64(c) => vihaco::Reset::reset(c), - Self::Bits128(c) => vihaco::Reset::reset(c), - Self::Bits256(c) => vihaco::Reset::reset(c), - Self::Bits512(c) => vihaco::Reset::reset(c), - Self::Bits1024(c) => vihaco::Reset::reset(c), - Self::Bits2048(c) => vihaco::Reset::reset(c), - } - } - - fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - macro_rules! build { - ($variant:ident, $N:literal) => {{ - let mut state = PauliSum::>::builder() - .n_qubits(info.n_qubits) - .strategy(paulisum_strategy(info)) - .build(); - for (word, coef) in terms { - state += (word.as_str(), *coef); - } - let initial_state = state.clone(); - let psum = PauliSumBackend::> { - state, - initial_state, - }; - Ok(Self::$variant(CircuitExecutor { backend: psum })) - }}; - } - if info.n_qubits <= 64 { - build!(Bits64, 8) - } else if info.n_qubits <= 128 { - build!(Bits128, 16) - } else if info.n_qubits <= 256 { - build!(Bits256, 32) - } else if info.n_qubits <= 512 { - build!(Bits512, 64) - } else if info.n_qubits <= 1024 { - build!(Bits1024, 128) - } else if info.n_qubits <= MAX_QUBITS { - build!(Bits2048, 256) - } else { - Err(eyre!( - "cannot simulate {} qubits: maximum is {MAX_QUBITS}", - info.n_qubits - )) - } - } -} - -pub(crate) enum CircuitKind { - Tableau(TableauBuckets), - PauliSum(PauliSumBuckets), - LossyPauliSum(LossyPauliSumBuckets), -} - -impl CircuitKind { - fn reset(&mut self) { - match self { - Self::Tableau(b) => b.reset(), - Self::PauliSum(b) => b.reset(), - Self::LossyPauliSum(b) => b.reset(), - } - } -} - -/// circuit component that selects qubit size from a runtime value and can do all backends -#[component] -#[instructions { circuit }] -pub struct Circuit { - inner: CircuitKind, -} - -impl Execute for Circuit { - type Message = I::Message; - type Effect = I::Effect; - type Error = eyre::Report; - - fn execute(&mut self, instruction: &I, msg: I::Message) -> Result { - match &mut self.inner { - CircuitKind::Tableau(b) => b.apply(instruction, msg), - CircuitKind::PauliSum(b) => b.apply(instruction, msg), - CircuitKind::LossyPauliSum(b) => b.apply(instruction, msg), - } - } -} - -/// Tableau-only component with runtime configured size -#[component] -#[instructions { circuit }] -pub struct TableauCircuit { - inner: TableauBuckets, -} - -impl Execute for TableauCircuit { - type Message = I::Message; - type Effect = I::Effect; - type Error = eyre::Report; - - fn execute( - &mut self, - instruction: &I, - message: Self::Message, - ) -> Result { - self.inner.apply(instruction, message) - } -} - -impl vihaco::Reset for Circuit { - fn reset(&mut self) { - self.inner.reset() - } -} - -impl vihaco::Reset for TableauCircuit { - fn reset(&mut self) { - self.inner.reset() - } -} - -impl Circuit { - /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and - /// `coefficient_threshold` from `info`; no observable required. - pub fn tableau(info: &PPVMDeviceInfo) -> Result { - let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; - Ok(Self { - inner: CircuitKind::Tableau(tab), - }) - } - - /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically - /// so a shot is reproducible. - pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { - let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; - Ok(Self { - inner: CircuitKind::Tableau(tab), - }) - } - - /// Build a PauliSum-backed circuit, seeding the state with every term in - /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the - /// caller is responsible for having parsed/validated the words against - /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). - pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - let psum = PauliSumBuckets::new(info, terms)?; - Ok(Self { - inner: CircuitKind::PauliSum(psum), - }) - } - - /// Build a LossyPauliSum-backed circuit. Same contract as - /// [`Circuit::paulisum`]. - pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - let psum = LossyPauliSumBuckets::new(info, terms)?; - Ok(Self { - inner: CircuitKind::LossyPauliSum(psum), - }) - } -} - -impl Default for Circuit { - fn default() -> Self { - // Default backend is Tableau with 0 qubits, which always fits the - // smallest bucket, so construction here is infallible. - Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") - } -} - -impl TableauCircuit { - /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and - /// `coefficient_threshold` from `info`; no observable required. - pub fn new(info: &PPVMDeviceInfo) -> Result { - let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; - Ok(Self { inner: tab }) - } - - /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically - /// so a shot is reproducible. - pub fn new_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { - let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; - Ok(Self { inner: tab }) - } -} - -impl Default for TableauCircuit { - fn default() -> Self { - Self::new(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") - } -} diff --git a/crates/ppvm-vihaco/src/component/backend.rs b/crates/ppvm-vihaco/src/component/backend.rs new file mode 100644 index 000000000..e180abadb --- /dev/null +++ b/crates/ppvm-vihaco/src/component/backend.rs @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::device_info::PPVMDeviceInfo; +use crate::measurements::MeasurementOutcome; +use bitvec::view::BitView; +use eyre::eyre; +use num::PrimInt; +use num::complex::Complex64; +use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; +use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; +use ppvm_tableau::prelude::*; + +/// Largest qubit count any backend can simulate. The widest size bucket is +/// backed by 2048-bit integers (`U2048` / `[u8; 256]`), so every constructor +/// rejects `n_qubits > MAX_QUBITS` rather than panicking. +pub const MAX_QUBITS: usize = 2048; + +/// Truncation strategy used by every `PauliSum` / `LossyPauliSum` size bucket. +/// Coefficient-threshold pruning is always on; the Pauli-weight cap is set per +/// run from the header (defaults to `usize::MAX` = no cap). +pub(crate) type PauliSumStrategy = CombinedStrategy; + +/// `PauliSum`'s `T` for the lossless backend: `[u8; N]` storage, fx hash, +/// f64 coefficients, the strategy above. +pub(crate) type PauliSumConfig = ByteFxHashF64; + +/// Same as `PauliSumConfig` but with `LossyPauliWord` as the word type, so the +/// loss-channel methods are dispatchable on the resulting `PauliSum`. +/// `LossyPauliWord`'s second type parameter (hasher) defaults to +/// `fxhash::FxBuildHasher`, matching `ByteFxHashF64`'s internal hasher. +pub(crate) type LossyPauliSumConfig = + ByteFxHashF64>; + +/// Build a `PauliSumStrategy` value from a `PPVMDeviceInfo`. Pulled out so the +/// six size-bucket constructors don't each repeat the strategy spelling. +pub(crate) fn paulisum_strategy(info: &PPVMDeviceInfo) -> PauliSumStrategy { + CombinedStrategy( + CoefficientThreshold(info.coefficient_threshold), + MaxPauliWeight(info.max_pauli_weight.unwrap_or(usize::MAX)), + ) +} + +/// Trait defining all methods required to implement the backend of the VM component +pub trait CircuitBackend { + type Config: Config; + type State: Clifford + + CliffordExtensions + + Reset + + TGate + + U3Gate + + RotationOne + + RotXY + + RotationTwo + + Depolarizing + + Depolarizing2 + + PauliError + + TwoQubitPauliError + + LossChannel + + CorrelatedLossChannel; + + fn state(&mut self) -> &mut Self::State; + + // NOTE: methods that differ between backends get dedicated methods + + fn measure(&mut self, _addr0: usize) -> eyre::Result { + Err(eyre!("measure is not supported on this backend")) + } + + fn leakage(&mut self, _addr0: usize, p0: f64, p1: f64) -> eyre::Result<()> { + Err(eyre!("leakage is not supported on this backend")) + } + + fn truncate(&mut self); + + fn reset(&mut self); +} + +impl CircuitBackend for GeneralizedTableau +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + type Config = T; + type State = GeneralizedTableau; + + fn state(&mut self) -> &mut Self::State { + self + } + + fn measure(&mut self, q: usize) -> eyre::Result { + let m = LossyMeasure::measure(self, q); + let e = match m { + Some(b) => { + if b { + MeasurementOutcome::One + } else { + MeasurementOutcome::Zero + } + } + None => MeasurementOutcome::Lost, + }; + Ok(e) + } + + fn truncate(&mut self) {} + + fn reset(&mut self) { + self.reset_all(); + } + + fn leakage(&mut self, _addr0: usize, p0: f64, p1: f64) -> eyre::Result<()> { + self.leakage_channel(_addr0, p0, p1); + Ok(()) + } +} + +/// PauliSum that also stores the initial observable so we can reset the component +pub struct PauliSumBackend> { + pub state: PauliSum, + pub(crate) initial_state: PauliSum, +} + +impl> PauliSumBackend { + pub fn new(observable: PauliSum) -> Self { + Self { + state: observable.clone(), + initial_state: observable.clone(), + } + } +} + +impl CircuitBackend for PauliSumBackend +where + T: Config, +{ + type Config = T; + type State = PauliSum; + + fn state(&mut self) -> &mut Self::State { + &mut self.state + } + + fn reset(&mut self) { + self.state = self.initial_state.clone(); + } + + fn truncate(&mut self) { + self.state.truncate(); + } +} diff --git a/crates/ppvm-vihaco/src/component/buckets.rs b/crates/ppvm-vihaco/src/component/buckets.rs new file mode 100644 index 000000000..9948bb005 --- /dev/null +++ b/crates/ppvm-vihaco/src/component/buckets.rs @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use super::CircuitExecutor; +use super::backend::{ + LossyPauliSumConfig, MAX_QUBITS, PauliSumBackend, PauliSumConfig, paulisum_strategy, +}; +use super::ops::CircuitOp; +use crate::device_info::PPVMDeviceInfo; +use bnum::types::{U256, U512, U1024, U2048}; +use eyre::{Result, eyre}; +use num::complex::Complex64; +use ppvm_pauli_sum::config::fx64hash::Byte8F64; +use ppvm_tableau::prelude::*; + +// TODO: enum dispatch can go once we only set the index type in tableau #204 +pub(crate) enum TableauBuckets { + Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>>), + Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>>), + Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>>), + Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>>), + Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>>), + Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>>), +} + +impl TableauBuckets { + pub(crate) fn apply( + &mut self, + op: &I, + msg: I::Message, + ) -> eyre::Result { + match self { + Self::Bits64(c) => op.apply(&mut c.backend, msg), + Self::Bits128(c) => op.apply(&mut c.backend, msg), + Self::Bits256(c) => op.apply(&mut c.backend, msg), + Self::Bits512(c) => op.apply(&mut c.backend, msg), + Self::Bits1024(c) => op.apply(&mut c.backend, msg), + Self::Bits2048(c) => op.apply(&mut c.backend, msg), + } + } + + pub(crate) fn reset(&mut self) { + match self { + Self::Bits64(c) => vihaco::Reset::reset(c), + Self::Bits128(c) => vihaco::Reset::reset(c), + Self::Bits256(c) => vihaco::Reset::reset(c), + Self::Bits512(c) => vihaco::Reset::reset(c), + Self::Bits1024(c) => vihaco::Reset::reset(c), + Self::Bits2048(c) => vihaco::Reset::reset(c), + } + } + + pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Result { + if n_qubits <= 64 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits64(CircuitExecutor { backend: tab })) + } else if n_qubits <= 128 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits128(CircuitExecutor { backend: tab })) + } else if n_qubits <= 256 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits256(CircuitExecutor { backend: tab })) + } else if n_qubits <= 512 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits512(CircuitExecutor { backend: tab })) + } else if n_qubits <= 1024 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits1024(CircuitExecutor { backend: tab })) + } else if n_qubits <= MAX_QUBITS { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Ok(Self::Bits2048(CircuitExecutor { backend: tab })) + } else { + Err(eyre!( + "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" + )) + } + } + + /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a + /// shot is reproducible. + pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Result { + macro_rules! seeded { + ($variant:ident) => {{ + let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); + Ok(Self::$variant(CircuitExecutor { backend: tab })) + }}; + } + if n_qubits <= 64 { + seeded!(Bits64) + } else if n_qubits <= 128 { + seeded!(Bits128) + } else if n_qubits <= 256 { + seeded!(Bits256) + } else if n_qubits <= 512 { + seeded!(Bits512) + } else if n_qubits <= 1024 { + seeded!(Bits1024) + } else if n_qubits <= MAX_QUBITS { + seeded!(Bits2048) + } else { + Err(eyre!( + "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" + )) + } + } +} + +pub(crate) enum PauliSumBuckets { + Bits64(CircuitExecutor>>), + Bits128(CircuitExecutor>>), + Bits256(CircuitExecutor>>), + Bits512(CircuitExecutor>>), + Bits1024(CircuitExecutor>>), + Bits2048(CircuitExecutor>>), +} + +impl PauliSumBuckets { + pub(crate) fn apply( + &mut self, + op: &I, + msg: I::Message, + ) -> eyre::Result { + match self { + Self::Bits64(c) => op.apply(&mut c.backend, msg), + Self::Bits128(c) => op.apply(&mut c.backend, msg), + Self::Bits256(c) => op.apply(&mut c.backend, msg), + Self::Bits512(c) => op.apply(&mut c.backend, msg), + Self::Bits1024(c) => op.apply(&mut c.backend, msg), + Self::Bits2048(c) => op.apply(&mut c.backend, msg), + } + } + + pub(crate) fn reset(&mut self) { + match self { + Self::Bits64(c) => vihaco::Reset::reset(c), + Self::Bits128(c) => vihaco::Reset::reset(c), + Self::Bits256(c) => vihaco::Reset::reset(c), + Self::Bits512(c) => vihaco::Reset::reset(c), + Self::Bits1024(c) => vihaco::Reset::reset(c), + Self::Bits2048(c) => vihaco::Reset::reset(c), + } + } + + /// Build a PauliSum-backed circuit, seeding the state with every term: + /// `for (word, coef) in terms { state += (word, coef); }`. Words must + /// already be validated against `info.n_qubits` by the caller. + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let mut state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } + let initial_state = state.clone(); + let psum = PauliSumBackend::> { + state, + initial_state, + }; + Ok(Self::$variant(CircuitExecutor { backend: psum })) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= MAX_QUBITS { + build!(Bits2048, 256) + } else { + Err(eyre!( + "cannot simulate {} qubits: maximum is {MAX_QUBITS}", + info.n_qubits + )) + } + } +} + +pub(crate) enum LossyPauliSumBuckets { + Bits64(CircuitExecutor>>), + Bits128(CircuitExecutor>>), + Bits256(CircuitExecutor>>), + Bits512(CircuitExecutor>>), + Bits1024(CircuitExecutor>>), + Bits2048(CircuitExecutor>>), +} + +impl LossyPauliSumBuckets { + pub(crate) fn apply( + &mut self, + op: &I, + msg: I::Message, + ) -> eyre::Result { + match self { + Self::Bits64(c) => op.apply(&mut c.backend, msg), + Self::Bits128(c) => op.apply(&mut c.backend, msg), + Self::Bits256(c) => op.apply(&mut c.backend, msg), + Self::Bits512(c) => op.apply(&mut c.backend, msg), + Self::Bits1024(c) => op.apply(&mut c.backend, msg), + Self::Bits2048(c) => op.apply(&mut c.backend, msg), + } + } + + pub(crate) fn reset(&mut self) { + match self { + Self::Bits64(c) => vihaco::Reset::reset(c), + Self::Bits128(c) => vihaco::Reset::reset(c), + Self::Bits256(c) => vihaco::Reset::reset(c), + Self::Bits512(c) => vihaco::Reset::reset(c), + Self::Bits1024(c) => vihaco::Reset::reset(c), + Self::Bits2048(c) => vihaco::Reset::reset(c), + } + } + + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let mut state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } + let initial_state = state.clone(); + let psum = PauliSumBackend::> { + state, + initial_state, + }; + Ok(Self::$variant(CircuitExecutor { backend: psum })) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= MAX_QUBITS { + build!(Bits2048, 256) + } else { + Err(eyre!( + "cannot simulate {} qubits: maximum is {MAX_QUBITS}", + info.n_qubits + )) + } + } +} + +pub(crate) enum CircuitKind { + Tableau(TableauBuckets), + PauliSum(PauliSumBuckets), + LossyPauliSum(LossyPauliSumBuckets), +} + +impl CircuitKind { + pub(crate) fn reset(&mut self) { + match self { + Self::Tableau(b) => b.reset(), + Self::PauliSum(b) => b.reset(), + Self::LossyPauliSum(b) => b.reset(), + } + } +} diff --git a/crates/ppvm-vihaco/src/component/messages.rs b/crates/ppvm-vihaco/src/component/messages.rs new file mode 100644 index 000000000..e4f302ff0 --- /dev/null +++ b/crates/ppvm-vihaco/src/component/messages.rs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Custom circuit messages that don't fit vihaco's standard stack protocols. diff --git a/crates/ppvm-vihaco/src/component/mod.rs b/crates/ppvm-vihaco/src/component/mod.rs new file mode 100644 index 000000000..cdfd38b04 --- /dev/null +++ b/crates/ppvm-vihaco/src/component/mod.rs @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +mod backend; +mod buckets; +mod messages; +mod ops; + +pub use backend::{CircuitBackend, MAX_QUBITS, PauliSumBackend}; +pub use ops::CircuitOp; + +use buckets::{CircuitKind, LossyPauliSumBuckets, PauliSumBuckets, TableauBuckets}; +use crate::device_info::PPVMDeviceInfo; +use eyre::Result; +use vihaco::{Execute, component}; +use vihaco_circuit_isa::circuit; + +/// Generic backend component - use this if you need one concrete variant in your machine +#[component] +#[instructions { circuit }] +pub struct CircuitExecutor { + pub backend: B, +} + +impl vihaco::Reset for CircuitExecutor { + fn reset(&mut self) { + self.backend.reset(); + } +} + +// The single execute impl for all CircuitOps +impl Execute for CircuitExecutor { + type Effect = I::Effect; + type Message = I::Message; + type Error = eyre::Report; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result { + I::apply(instruction, &mut self.backend, message) + } +} + +/// circuit component that selects qubit size from a runtime value and can do all backends +#[component] +#[instructions { circuit }] +pub struct Circuit { + inner: CircuitKind, +} + +impl Execute for Circuit { + type Message = I::Message; + type Effect = I::Effect; + type Error = eyre::Report; + + fn execute(&mut self, instruction: &I, msg: I::Message) -> Result { + match &mut self.inner { + CircuitKind::Tableau(b) => b.apply(instruction, msg), + CircuitKind::PauliSum(b) => b.apply(instruction, msg), + CircuitKind::LossyPauliSum(b) => b.apply(instruction, msg), + } + } +} + +/// Tableau-only component with runtime configured size +#[component] +#[instructions { circuit }] +pub struct TableauCircuit { + inner: TableauBuckets, +} + +impl Execute for TableauCircuit { + type Message = I::Message; + type Effect = I::Effect; + type Error = eyre::Report; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result { + self.inner.apply(instruction, message) + } +} + +impl vihaco::Reset for Circuit { + fn reset(&mut self) { + self.inner.reset() + } +} + +impl vihaco::Reset for TableauCircuit { + fn reset(&mut self) { + self.inner.reset() + } +} + +impl Circuit { + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and + /// `coefficient_threshold` from `info`; no observable required. + pub fn tableau(info: &PPVMDeviceInfo) -> Result { + let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; + Ok(Self { + inner: CircuitKind::Tableau(tab), + }) + } + + /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { + let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; + Ok(Self { + inner: CircuitKind::Tableau(tab), + }) + } + + /// Build a PauliSum-backed circuit, seeding the state with every term in + /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the + /// caller is responsible for having parsed/validated the words against + /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). + pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + let psum = PauliSumBuckets::new(info, terms)?; + Ok(Self { + inner: CircuitKind::PauliSum(psum), + }) + } + + /// Build a LossyPauliSum-backed circuit. Same contract as + /// [`Circuit::paulisum`]. + pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + let psum = LossyPauliSumBuckets::new(info, terms)?; + Ok(Self { + inner: CircuitKind::LossyPauliSum(psum), + }) + } +} + +impl Default for Circuit { + fn default() -> Self { + // Default backend is Tableau with 0 qubits, which always fits the + // smallest bucket, so construction here is infallible. + Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") + } +} + +impl TableauCircuit { + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and + /// `coefficient_threshold` from `info`; no observable required. + pub fn new(info: &PPVMDeviceInfo) -> Result { + let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; + Ok(Self { inner: tab }) + } + + /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn new_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { + let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; + Ok(Self { inner: tab }) + } +} + +impl Default for TableauCircuit { + fn default() -> Self { + Self::new(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") + } +} diff --git a/crates/ppvm-vihaco/src/component/ops.rs b/crates/ppvm-vihaco/src/component/ops.rs new file mode 100644 index 000000000..59e29c872 --- /dev/null +++ b/crates/ppvm-vihaco/src/component/ops.rs @@ -0,0 +1,253 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use super::backend::CircuitBackend; +use crate::measurements::{CircuitOutcomeEffect, MeasurementEffect}; +use ppvm_tableau::prelude::*; +use vihaco::{Effects, PopFromStack}; +use vihaco_circuit_isa::circuit; + +macro_rules! batch_for { + ($tab:expr, $method:ident, $addrs:expr) => { + for addr in $addrs { $tab.$method(*addr); } + }; + ($tab:expr, $method:ident, $addrs:expr, $($arg:expr),+) => { + for addr in $addrs { $tab.$method(*addr, $($arg),+); } + }; +} + +/// Two-qubit sibling of [`batch_for!`]: drives a method that takes two qubit +/// addresses (plus optional extra args) over a slice of `(usize, usize)` pairs. +macro_rules! batch_pairs_for { + ($state:expr, $method:ident, $pairs:expr) => { + for &(a, b) in $pairs { $state.$method(a, b); } + }; + ($state:expr, $method:ident, $pairs:expr, $($arg:expr),+) => { + for &(a, b) in $pairs { $state.$method(a, b, $($arg),+); } + }; +} + +// CircuitOp trait: define how an instruction executes on a generic backend +// This is used to avoid having huge where constraints on the enums later +// - without this trait, we'd have to constrain every enum variant to all Execute impls +// TODO: re-evaluate whether this is needed once we refactor and try to get rid of enums +pub trait CircuitOp { + type Message; + type Effect; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result; +} + +macro_rules! single_qubit_execute { + ($leaf:ident, $method:ident) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = PopFromStack<(u64,)>; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend.state().$method(msg.0.0 as usize); + Ok(()) + } + } + }; +} + +macro_rules! two_qubit_execute { + ($leaf:ident, $method:ident) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = PopFromStack<(u64, u64)>; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend.state().$method(msg.0.0 as usize, msg.0.1 as usize); + Ok(()) + } + } + }; +} + +macro_rules! single_qubit_rotation_execute { + ($leaf:ident, $method:ident) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = PopFromStack<(u64, f64)>; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend.state().$method(msg.0.0 as usize, msg.0.1); + Ok(()) + } + } + }; +} + +macro_rules! two_qubit_rotation_execute { + ($leaf:ident, $method:ident) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = PopFromStack<(u64, u64, f64)>; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend + .state() + .$method(msg.0.0 as usize, msg.0.1 as usize, msg.0.2); + Ok(()) + } + } + }; +} + +single_qubit_execute!(X, x); +single_qubit_execute!(Y, y); +single_qubit_execute!(Z, z); +single_qubit_execute!(T, t); +single_qubit_execute!(S, s); +single_qubit_execute!(TAdj, t_dag); +single_qubit_execute!(SAdj, s_dag); +single_qubit_execute!(SqrtX, sqrt_x); +single_qubit_execute!(H, h); +single_qubit_execute!(SqrtXAdj, sqrt_x_dag); +single_qubit_execute!(SqrtY, sqrt_y); +single_qubit_execute!(SqrtYAdj, sqrt_y_dag); + +two_qubit_execute!(CNOT, cnot); +two_qubit_execute!(CZ, cz); + +single_qubit_rotation_execute!(RX, rx); +single_qubit_rotation_execute!(RY, ry); +single_qubit_rotation_execute!(RZ, rz); + +two_qubit_rotation_execute!(RXX, rxx); +two_qubit_rotation_execute!(RYY, ryy); +two_qubit_rotation_execute!(RZZ, rzz); + +single_qubit_execute!(Reset, reset); +single_qubit_rotation_execute!(Depolarize, depolarize1); +single_qubit_rotation_execute!(Loss, loss_channel); +two_qubit_rotation_execute!(Depolarize2, depolarize2); + +// TODO: batched gates; need custom messages + +impl CircuitOp for circuit::U3 { + type Effect = (); + type Message = PopFromStack<(u64, f64, f64, f64)>; + + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend + .state() + .u3(msg.0.0 as usize, msg.0.1, msg.0.2, msg.0.3); + Ok(()) + } +} + +impl CircuitOp for circuit::R { + type Effect = (); + type Message = PopFromStack<(u64, f64, f64)>; + + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend.state().r(msg.0.0 as usize, msg.0.1, msg.0.2); + Ok(()) + } +} + +impl CircuitOp for circuit::Leakage { + type Effect = (); + type Message = PopFromStack<(u64, f64, f64)>; + + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend.leakage(msg.0.0 as usize, msg.0.1, msg.0.2)?; + Ok(()) + } +} + +impl CircuitOp for circuit::PauliError { + type Effect = (); + type Message = PopFromStack<(u64, f64, f64, f64)>; + + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend + .state() + .pauli_error(msg.0.0 as usize, [msg.0.1, msg.0.2, msg.0.3]); + Ok(()) + } +} + +impl CircuitOp for circuit::TwoQubitPauliError { + type Effect = (); + type Message = PopFromStack<(u64, u64, f64, f64)>; + + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + // TODO: probably needs a custom message since we can't pop float arrays + unimplemented!("TODO: needs a custom message") + } +} + +impl CircuitOp for circuit::CorrelatedLoss { + type Effect = (); + type Message = PopFromStack<(u64, u64, f64, f64, f64)>; + + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + backend.state().correlated_loss_channel( + msg.0.0 as usize, + msg.0.1 as usize, + [msg.0.2, msg.0.3, msg.0.4], + ); + Ok(()) + } +} + +impl CircuitOp for circuit::Measure { + type Effect = Effects; + type Message = PopFromStack<(u64,)>; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result { + let outcome = backend.measure(msg.0.0 as usize)?; + Ok(Effects::one(CircuitOutcomeEffect::Measurement( + MeasurementEffect { + measurement_results: smallvec::smallvec![outcome], + }, + ))) + } +} + +impl CircuitOp for circuit::Truncate { + type Effect = (); + type Message = vihaco::NoMessage; + + fn apply(&self, backend: &mut B, _: Self::Message) -> eyre::Result<()> { + backend.truncate(); + Ok(()) + } +} + +impl CircuitOp for circuit::Trace { + type Effect = (); + type Message = vihaco::NoMessage; + + fn apply(&self, backend: &mut B, _: Self::Message) -> eyre::Result<()> { + unimplemented!("TODO: needs a custom message"); + } +} From 4ba7386c40818ad289bcfa8aa22e32e47a63aeec Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 14:39:24 +0100 Subject: [PATCH 17/36] Split out stuff from mod.rs and start adding custom messages --- crates/ppvm-vihaco/src/component/executors.rs | 162 ++++++++++++++++++ crates/ppvm-vihaco/src/component/messages.rs | 66 +++++++ crates/ppvm-vihaco/src/component/mod.rs | 162 +----------------- crates/ppvm-vihaco/src/lib.rs | 10 +- 4 files changed, 238 insertions(+), 162 deletions(-) create mode 100644 crates/ppvm-vihaco/src/component/executors.rs diff --git a/crates/ppvm-vihaco/src/component/executors.rs b/crates/ppvm-vihaco/src/component/executors.rs new file mode 100644 index 000000000..8d9355445 --- /dev/null +++ b/crates/ppvm-vihaco/src/component/executors.rs @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use super::backend::CircuitBackend; +use super::buckets::{CircuitKind, LossyPauliSumBuckets, PauliSumBuckets, TableauBuckets}; +use super::ops::CircuitOp; +use crate::device_info::PPVMDeviceInfo; +use eyre::Result; +use vihaco::{Execute, component}; +use vihaco_circuit_isa::circuit; + +/// Generic backend component - use this if you need one concrete variant in your machine +#[component] +#[instructions { circuit }] +pub struct CircuitExecutor { + pub backend: B, +} + +impl vihaco::Reset for CircuitExecutor { + fn reset(&mut self) { + self.backend.reset(); + } +} + +// The single execute impl for all CircuitOps +impl Execute for CircuitExecutor { + type Effect = I::Effect; + type Message = I::Message; + type Error = eyre::Report; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result { + I::apply(instruction, &mut self.backend, message) + } +} + +/// circuit component that selects qubit size from a runtime value and can do all backends +#[component] +#[instructions { circuit }] +pub struct Circuit { + inner: CircuitKind, +} + +impl Execute for Circuit { + type Message = I::Message; + type Effect = I::Effect; + type Error = eyre::Report; + + fn execute(&mut self, instruction: &I, msg: I::Message) -> Result { + match &mut self.inner { + CircuitKind::Tableau(b) => b.apply(instruction, msg), + CircuitKind::PauliSum(b) => b.apply(instruction, msg), + CircuitKind::LossyPauliSum(b) => b.apply(instruction, msg), + } + } +} + +/// Tableau-only component with runtime configured size +#[component] +#[instructions { circuit }] +pub struct TableauCircuit { + inner: TableauBuckets, +} + +impl Execute for TableauCircuit { + type Message = I::Message; + type Effect = I::Effect; + type Error = eyre::Report; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result { + self.inner.apply(instruction, message) + } +} + +impl vihaco::Reset for Circuit { + fn reset(&mut self) { + self.inner.reset() + } +} + +impl vihaco::Reset for TableauCircuit { + fn reset(&mut self) { + self.inner.reset() + } +} + +impl Circuit { + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and + /// `coefficient_threshold` from `info`; no observable required. + pub fn tableau(info: &PPVMDeviceInfo) -> Result { + let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; + Ok(Self { + inner: CircuitKind::Tableau(tab), + }) + } + + /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { + let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; + Ok(Self { + inner: CircuitKind::Tableau(tab), + }) + } + + /// Build a PauliSum-backed circuit, seeding the state with every term in + /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the + /// caller is responsible for having parsed/validated the words against + /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). + pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + let psum = PauliSumBuckets::new(info, terms)?; + Ok(Self { + inner: CircuitKind::PauliSum(psum), + }) + } + + /// Build a LossyPauliSum-backed circuit. Same contract as + /// [`Circuit::paulisum`]. + pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + let psum = LossyPauliSumBuckets::new(info, terms)?; + Ok(Self { + inner: CircuitKind::LossyPauliSum(psum), + }) + } +} + +impl Default for Circuit { + fn default() -> Self { + // Default backend is Tableau with 0 qubits, which always fits the + // smallest bucket, so construction here is infallible. + Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") + } +} + +impl TableauCircuit { + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and + /// `coefficient_threshold` from `info`; no observable required. + pub fn new(info: &PPVMDeviceInfo) -> Result { + let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; + Ok(Self { inner: tab }) + } + + /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn new_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { + let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; + Ok(Self { inner: tab }) + } +} + +impl Default for TableauCircuit { + fn default() -> Self { + Self::new(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") + } +} diff --git a/crates/ppvm-vihaco/src/component/messages.rs b/crates/ppvm-vihaco/src/component/messages.rs index e4f302ff0..497b90f98 100644 --- a/crates/ppvm-vihaco/src/component/messages.rs +++ b/crates/ppvm-vihaco/src/component/messages.rs @@ -2,3 +2,69 @@ // SPDX-License-Identifier: Apache-2.0 //! Custom circuit messages that don't fit vihaco's standard stack protocols. + +use smallvec::SmallVec; +use vihaco::PopStack; +use vihaco::word::Word; + +/// Qubit batch plus a fixed parameter block; `P = ()` for plain batches. +pub struct QubitBatch

{ + pub qubits: SmallVec<[u64; 8]>, + pub params: P, +} + +pub struct TwoQubitPauliErrorMsg { + pub q0: u64, + pub q1: u64, + pub ps: [f64; 15], +} + +pub struct TraceMessage { + pub pauli_pattern: String, +} + +/// Fixed-size parameter blocks popped off the operand stack. +pub trait PopParams: Sized { + fn pop_params>(source: &mut S) -> eyre::Result; +} + +impl PopParams for () { + fn pop_params>(_: &mut S) -> eyre::Result { + Ok(()) + } +} + +impl PopParams for f64 { + fn pop_params>(source: &mut S) -> eyre::Result { + source.pop() + } +} + +impl PopParams for [f64; N] { + fn pop_params>(source: &mut S) -> eyre::Result { + let mut params = [0.0; N]; + // Last pushed is on top, so fill from the back to keep push order. + for p in params.iter_mut().rev() { + *p = source.pop()?; + } + Ok(params) + } +} + +/// Pop a [`QubitBatch`] pushed as `q0 .. q(n-1)`, then `n`, then the params. +/// +/// Call this from a composite's `BuildMessage` impl for batched instructions. +pub fn pop_qubit_batch(stack: &mut S) -> eyre::Result> +where + S: PopStack + PopStack, + P: PopParams, +{ + let params = P::pop_params(stack)?; + let n = usize::try_from(PopStack::::pop(stack)?)?; + let mut qubits = SmallVec::with_capacity(n); + for _ in 0..n { + qubits.push(PopStack::::pop(stack)?); + } + qubits.reverse(); + Ok(QubitBatch { qubits, params }) +} diff --git a/crates/ppvm-vihaco/src/component/mod.rs b/crates/ppvm-vihaco/src/component/mod.rs index cdfd38b04..cf439572b 100644 --- a/crates/ppvm-vihaco/src/component/mod.rs +++ b/crates/ppvm-vihaco/src/component/mod.rs @@ -3,166 +3,14 @@ mod backend; mod buckets; +mod executors; mod messages; mod ops; pub use backend::{CircuitBackend, MAX_QUBITS, PauliSumBackend}; +pub use executors::{Circuit, CircuitExecutor, TableauCircuit}; +pub use messages::{PopParams, QubitBatch, TraceMessage, TwoQubitPauliErrorMsg, pop_qubit_batch}; pub use ops::CircuitOp; -use buckets::{CircuitKind, LossyPauliSumBuckets, PauliSumBuckets, TableauBuckets}; -use crate::device_info::PPVMDeviceInfo; -use eyre::Result; -use vihaco::{Execute, component}; -use vihaco_circuit_isa::circuit; - -/// Generic backend component - use this if you need one concrete variant in your machine -#[component] -#[instructions { circuit }] -pub struct CircuitExecutor { - pub backend: B, -} - -impl vihaco::Reset for CircuitExecutor { - fn reset(&mut self) { - self.backend.reset(); - } -} - -// The single execute impl for all CircuitOps -impl Execute for CircuitExecutor { - type Effect = I::Effect; - type Message = I::Message; - type Error = eyre::Report; - - fn execute( - &mut self, - instruction: &I, - message: Self::Message, - ) -> Result { - I::apply(instruction, &mut self.backend, message) - } -} - -/// circuit component that selects qubit size from a runtime value and can do all backends -#[component] -#[instructions { circuit }] -pub struct Circuit { - inner: CircuitKind, -} - -impl Execute for Circuit { - type Message = I::Message; - type Effect = I::Effect; - type Error = eyre::Report; - - fn execute(&mut self, instruction: &I, msg: I::Message) -> Result { - match &mut self.inner { - CircuitKind::Tableau(b) => b.apply(instruction, msg), - CircuitKind::PauliSum(b) => b.apply(instruction, msg), - CircuitKind::LossyPauliSum(b) => b.apply(instruction, msg), - } - } -} - -/// Tableau-only component with runtime configured size -#[component] -#[instructions { circuit }] -pub struct TableauCircuit { - inner: TableauBuckets, -} - -impl Execute for TableauCircuit { - type Message = I::Message; - type Effect = I::Effect; - type Error = eyre::Report; - - fn execute( - &mut self, - instruction: &I, - message: Self::Message, - ) -> Result { - self.inner.apply(instruction, message) - } -} - -impl vihaco::Reset for Circuit { - fn reset(&mut self) { - self.inner.reset() - } -} - -impl vihaco::Reset for TableauCircuit { - fn reset(&mut self) { - self.inner.reset() - } -} - -impl Circuit { - /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and - /// `coefficient_threshold` from `info`; no observable required. - pub fn tableau(info: &PPVMDeviceInfo) -> Result { - let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; - Ok(Self { - inner: CircuitKind::Tableau(tab), - }) - } - - /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically - /// so a shot is reproducible. - pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { - let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; - Ok(Self { - inner: CircuitKind::Tableau(tab), - }) - } - - /// Build a PauliSum-backed circuit, seeding the state with every term in - /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the - /// caller is responsible for having parsed/validated the words against - /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). - pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - let psum = PauliSumBuckets::new(info, terms)?; - Ok(Self { - inner: CircuitKind::PauliSum(psum), - }) - } - - /// Build a LossyPauliSum-backed circuit. Same contract as - /// [`Circuit::paulisum`]. - pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { - let psum = LossyPauliSumBuckets::new(info, terms)?; - Ok(Self { - inner: CircuitKind::LossyPauliSum(psum), - }) - } -} - -impl Default for Circuit { - fn default() -> Self { - // Default backend is Tableau with 0 qubits, which always fits the - // smallest bucket, so construction here is infallible. - Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") - } -} - -impl TableauCircuit { - /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and - /// `coefficient_threshold` from `info`; no observable required. - pub fn new(info: &PPVMDeviceInfo) -> Result { - let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; - Ok(Self { inner: tab }) - } - - /// Same as [`Circuit.tableau`], but seed the tableau RNG deterministically - /// so a shot is reproducible. - pub fn new_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { - let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; - Ok(Self { inner: tab }) - } -} - -impl Default for TableauCircuit { - fn default() -> Self { - Self::new(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") - } -} +// vihaco resolves `#[device]` children through these generated companions. +pub use executors::*; diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 8241c5496..c59a66b15 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -78,11 +78,11 @@ use vihaco::{Type, Value, module::LocalModule}; // dump_program(&program, output_path) // } -// pub mod prelude { -// // pub use crate::component::Circuit; -// pub use crate::composite::PPVM; -// pub use crate::syntax::{PPVMHeader, PPVMResolver}; -// } +pub mod prelude { + pub use crate::component::Circuit; + pub use crate::composite::PPVM; + // pub use crate::syntax::{PPVMHeader, PPVMResolver}; +} // #[cfg(test)] // mod tests { From b644d6ffe9a8b1f2cd8e4b4db27485c16fd40416 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 14:47:51 +0100 Subject: [PATCH 18/36] Add batch instructions and move message to vihaco-isa --- crates/ppvm-vihaco/src/component/mod.rs | 5 +- crates/vihaco-circuit-isa/src/lib.rs | 266 ++++++++++++++++++ .../src}/messages.rs | 0 3 files changed, 269 insertions(+), 2 deletions(-) rename crates/{ppvm-vihaco/src/component => vihaco-circuit-isa/src}/messages.rs (100%) diff --git a/crates/ppvm-vihaco/src/component/mod.rs b/crates/ppvm-vihaco/src/component/mod.rs index cf439572b..d5ef7e24c 100644 --- a/crates/ppvm-vihaco/src/component/mod.rs +++ b/crates/ppvm-vihaco/src/component/mod.rs @@ -4,13 +4,14 @@ mod backend; mod buckets; mod executors; -mod messages; mod ops; pub use backend::{CircuitBackend, MAX_QUBITS, PauliSumBackend}; pub use executors::{Circuit, CircuitExecutor, TableauCircuit}; -pub use messages::{PopParams, QubitBatch, TraceMessage, TwoQubitPauliErrorMsg, pop_qubit_batch}; pub use ops::CircuitOp; +pub use vihaco_circuit_isa::messages::{ + PopParams, QubitBatch, TraceMessage, TwoQubitPauliErrorMsg, pop_qubit_batch, +}; // vihaco resolves `#[device]` children through these generated companions. pub use executors::*; diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index de2b41e20..f68259819 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -3,6 +3,8 @@ use vihaco::dialect; +pub mod messages; + #[dialect] pub mod circuit { #[instruction] @@ -109,6 +111,124 @@ pub mod circuit { #[instruction] pub struct Leakage; + + // Batched variants: same gate applied to every qubit (or pair) in the batch. + + #[pattern = "'x_many"] + #[instruction] + pub struct XMany; + + #[pattern = "'y_many"] + #[instruction] + pub struct YMany; + + #[pattern = "'z_many"] + #[instruction] + pub struct ZMany; + + #[pattern = "'h_many"] + #[instruction] + pub struct HMany; + + #[pattern = "'s_many"] + #[instruction] + pub struct SMany; + + #[pattern = "'s_adj_many"] + #[instruction] + pub struct SAdjMany; + + #[pattern = "'t_many"] + #[instruction] + pub struct TMany; + + #[pattern = "'t_adj_many"] + #[instruction] + pub struct TAdjMany; + + #[pattern = "'sqrt_x_many"] + #[instruction] + pub struct SqrtXMany; + + #[pattern = "'sqrt_x_adj_many"] + #[instruction] + pub struct SqrtXAdjMany; + + #[pattern = "'sqrt_y_many"] + #[instruction] + pub struct SqrtYMany; + + #[pattern = "'sqrt_y_adj_many"] + #[instruction] + pub struct SqrtYAdjMany; + + #[pattern = "'reset_many"] + #[instruction] + pub struct ResetMany; + + #[pattern = "'measure_many"] + #[instruction] + pub struct MeasureMany; + + #[pattern = "'cnot_many"] + #[instruction] + pub struct CNOTMany; + + #[pattern = "'cz_many"] + #[instruction] + pub struct CZMany; + + #[pattern = "'rx_many"] + #[instruction] + pub struct RXMany; + + #[pattern = "'ry_many"] + #[instruction] + pub struct RYMany; + + #[pattern = "'rz_many"] + #[instruction] + pub struct RZMany; + + #[pattern = "'depolarize_many"] + #[instruction] + pub struct DepolarizeMany; + + #[pattern = "'loss_many"] + #[instruction] + pub struct LossMany; + + #[pattern = "'rxx_many"] + #[instruction] + pub struct RXXMany; + + #[pattern = "'ryy_many"] + #[instruction] + pub struct RYYMany; + + #[pattern = "'rzz_many"] + #[instruction] + pub struct RZZMany; + + #[pattern = "'depolarize2_many"] + #[instruction] + pub struct Depolarize2Many; + + #[pattern = "'u3_many"] + #[instruction] + pub struct U3Many; + + #[pattern = "'paulierror_many"] + #[instruction] + pub struct PauliErrorMany; + + #[pattern = "'correlatedloss_many"] + #[instruction] + pub struct CorrelatedLossMany; + + #[pattern = "'twoqubitpaulierror_many"] + #[instruction] + pub struct TwoQubitPauliErrorMany; } #[cfg(test)] @@ -198,4 +318,150 @@ mod tests { Ok(circuit::Leakage) ); } + + #[test] + fn parses_batched_tokens() { + assert_eq!( + circuit::XMany::parser().parse("x_many").into_result(), + Ok(circuit::XMany) + ); + assert_eq!( + circuit::YMany::parser().parse("y_many").into_result(), + Ok(circuit::YMany) + ); + assert_eq!( + circuit::ZMany::parser().parse("z_many").into_result(), + Ok(circuit::ZMany) + ); + assert_eq!( + circuit::HMany::parser().parse("h_many").into_result(), + Ok(circuit::HMany) + ); + assert_eq!( + circuit::SMany::parser().parse("s_many").into_result(), + Ok(circuit::SMany) + ); + assert_eq!( + circuit::SAdjMany::parser() + .parse("s_adj_many") + .into_result(), + Ok(circuit::SAdjMany) + ); + assert_eq!( + circuit::TMany::parser().parse("t_many").into_result(), + Ok(circuit::TMany) + ); + assert_eq!( + circuit::TAdjMany::parser() + .parse("t_adj_many") + .into_result(), + Ok(circuit::TAdjMany) + ); + assert_eq!( + circuit::SqrtXMany::parser() + .parse("sqrt_x_many") + .into_result(), + Ok(circuit::SqrtXMany) + ); + assert_eq!( + circuit::SqrtXAdjMany::parser() + .parse("sqrt_x_adj_many") + .into_result(), + Ok(circuit::SqrtXAdjMany) + ); + assert_eq!( + circuit::SqrtYMany::parser() + .parse("sqrt_y_many") + .into_result(), + Ok(circuit::SqrtYMany) + ); + assert_eq!( + circuit::SqrtYAdjMany::parser() + .parse("sqrt_y_adj_many") + .into_result(), + Ok(circuit::SqrtYAdjMany) + ); + assert_eq!( + circuit::ResetMany::parser() + .parse("reset_many") + .into_result(), + Ok(circuit::ResetMany) + ); + assert_eq!( + circuit::MeasureMany::parser() + .parse("measure_many") + .into_result(), + Ok(circuit::MeasureMany) + ); + assert_eq!( + circuit::CNOTMany::parser().parse("cnot_many").into_result(), + Ok(circuit::CNOTMany) + ); + assert_eq!( + circuit::CZMany::parser().parse("cz_many").into_result(), + Ok(circuit::CZMany) + ); + assert_eq!( + circuit::RXMany::parser().parse("rx_many").into_result(), + Ok(circuit::RXMany) + ); + assert_eq!( + circuit::RYMany::parser().parse("ry_many").into_result(), + Ok(circuit::RYMany) + ); + assert_eq!( + circuit::RZMany::parser().parse("rz_many").into_result(), + Ok(circuit::RZMany) + ); + assert_eq!( + circuit::DepolarizeMany::parser() + .parse("depolarize_many") + .into_result(), + Ok(circuit::DepolarizeMany) + ); + assert_eq!( + circuit::LossMany::parser().parse("loss_many").into_result(), + Ok(circuit::LossMany) + ); + assert_eq!( + circuit::RXXMany::parser().parse("rxx_many").into_result(), + Ok(circuit::RXXMany) + ); + assert_eq!( + circuit::RYYMany::parser().parse("ryy_many").into_result(), + Ok(circuit::RYYMany) + ); + assert_eq!( + circuit::RZZMany::parser().parse("rzz_many").into_result(), + Ok(circuit::RZZMany) + ); + assert_eq!( + circuit::Depolarize2Many::parser() + .parse("depolarize2_many") + .into_result(), + Ok(circuit::Depolarize2Many) + ); + assert_eq!( + circuit::U3Many::parser().parse("u3_many").into_result(), + Ok(circuit::U3Many) + ); + assert_eq!( + circuit::PauliErrorMany::parser() + .parse("paulierror_many") + .into_result(), + Ok(circuit::PauliErrorMany) + ); + assert_eq!( + circuit::CorrelatedLossMany::parser() + .parse("correlatedloss_many") + .into_result(), + Ok(circuit::CorrelatedLossMany) + ); + assert_eq!( + circuit::TwoQubitPauliErrorMany::parser() + .parse("twoqubitpaulierror_many") + .into_result(), + Ok(circuit::TwoQubitPauliErrorMany) + ); + } } diff --git a/crates/ppvm-vihaco/src/component/messages.rs b/crates/vihaco-circuit-isa/src/messages.rs similarity index 100% rename from crates/ppvm-vihaco/src/component/messages.rs rename to crates/vihaco-circuit-isa/src/messages.rs From ea73b6ee0bcbf496e533c1ceeb115769943d5828 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 15:06:24 +0100 Subject: [PATCH 19/36] Add impls for batched gates --- crates/ppvm-vihaco/src/component/backend.rs | 2 + crates/ppvm-vihaco/src/component/ops.rs | 182 +++++++++++++++++++- crates/vihaco-circuit-isa/src/messages.rs | 5 +- 3 files changed, 185 insertions(+), 4 deletions(-) diff --git a/crates/ppvm-vihaco/src/component/backend.rs b/crates/ppvm-vihaco/src/component/backend.rs index e180abadb..2f791eb05 100644 --- a/crates/ppvm-vihaco/src/component/backend.rs +++ b/crates/ppvm-vihaco/src/component/backend.rs @@ -46,6 +46,8 @@ pub trait CircuitBackend { type Config: Config; type State: Clifford + CliffordExtensions + + CliffordBatch + + CliffordExtensionsBatch + Reset + TGate + U3Gate diff --git a/crates/ppvm-vihaco/src/component/ops.rs b/crates/ppvm-vihaco/src/component/ops.rs index 59e29c872..c265954af 100644 --- a/crates/ppvm-vihaco/src/component/ops.rs +++ b/crates/ppvm-vihaco/src/component/ops.rs @@ -2,10 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use super::backend::CircuitBackend; -use crate::measurements::{CircuitOutcomeEffect, MeasurementEffect}; +use crate::measurements::{CircuitOutcomeEffect, MeasurementEffect, MeasurementResult}; use ppvm_tableau::prelude::*; +use smallvec::SmallVec; use vihaco::{Effects, PopFromStack}; use vihaco_circuit_isa::circuit; +use vihaco_circuit_isa::messages::QubitBatch; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { @@ -116,6 +118,24 @@ macro_rules! two_qubit_rotation_execute { }; } +macro_rules! single_qubit_batch_execute { + ($leaf:ident, $method:ident) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = vihaco_circuit_isa::messages::QubitBatch; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result { + backend.state().$method(&msg.qubits); + Ok(()) + } + } + }; +} + single_qubit_execute!(X, x); single_qubit_execute!(Y, y); single_qubit_execute!(Z, z); @@ -145,7 +165,165 @@ single_qubit_rotation_execute!(Depolarize, depolarize1); single_qubit_rotation_execute!(Loss, loss_channel); two_qubit_rotation_execute!(Depolarize2, depolarize2); -// TODO: batched gates; need custom messages +single_qubit_batch_execute!(XMany, x_many); +single_qubit_batch_execute!(YMany, y_many); +single_qubit_batch_execute!(ZMany, z_many); +single_qubit_batch_execute!(TMany, t_many); +single_qubit_batch_execute!(SMany, s_many); +single_qubit_batch_execute!(TAdjMany, t_dag_many); +single_qubit_batch_execute!(SAdjMany, s_dag_many); +single_qubit_batch_execute!(SqrtXMany, sqrt_x_many); +single_qubit_batch_execute!(HMany, h_many); +single_qubit_batch_execute!(SqrtXAdjMany, sqrt_x_dag_many); +single_qubit_batch_execute!(SqrtYMany, sqrt_y_many); +single_qubit_batch_execute!(SqrtYAdjMany, sqrt_y_dag_many); +single_qubit_batch_execute!(ResetMany, reset_many); + +/// Split a flat batch `[a0, b0, a1, b1, ..]` into `(a, b)` pairs. +fn pairs_of(qubits: &[usize]) -> eyre::Result> { + eyre::ensure!( + qubits.len() % 2 == 0, + "two-qubit batch needs an even number of qubits, got {}", + qubits.len() + ); + Ok(qubits.chunks_exact(2)) +} + +macro_rules! single_qubit_rotation_batch_execute { + ($leaf:ident, $method:ident) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = QubitBatch; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + backend.state().$method(&msg.qubits, msg.params); + Ok(()) + } + } + }; +} + +/// Batched op without a dedicated `*_many` method: applies `$method` per qubit. +macro_rules! single_qubit_param_loop_execute { + ($leaf:ident, $method:ident, $params:ty) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = QubitBatch<$params>; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + let state = backend.state(); + for &q in &msg.qubits { + state.$method(q, msg.params); + } + Ok(()) + } + } + }; +} + +macro_rules! two_qubit_batch_execute { + ($leaf:ident, $method:ident) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = QubitBatch; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + // `*_many` takes tuples, so the flat batch is copied into pairs. + let pairs: SmallVec<[(usize, usize); 4]> = + pairs_of(&msg.qubits)?.map(|p| (p[0], p[1])).collect(); + backend.state().$method(&pairs); + Ok(()) + } + } + }; +} + +/// Two-qubit counterpart of [`single_qubit_param_loop_execute!`]. +macro_rules! two_qubit_param_loop_execute { + ($leaf:ident, $method:ident, $params:ty) => { + impl CircuitOp for circuit::$leaf { + type Effect = (); + type Message = QubitBatch<$params>; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result<()> { + let state = backend.state(); + for p in pairs_of(&msg.qubits)? { + state.$method(p[0], p[1], msg.params); + } + Ok(()) + } + } + }; +} + +two_qubit_batch_execute!(CNOTMany, cnot_many); +two_qubit_batch_execute!(CZMany, cz_many); + +single_qubit_rotation_batch_execute!(RXMany, rx_many); +single_qubit_rotation_batch_execute!(RYMany, ry_many); +single_qubit_rotation_batch_execute!(RZMany, rz_many); + +single_qubit_param_loop_execute!(DepolarizeMany, depolarize1, f64); +single_qubit_param_loop_execute!(LossMany, loss_channel, f64); +single_qubit_param_loop_execute!(PauliErrorMany, pauli_error, [f64; 3]); + +two_qubit_param_loop_execute!(RXXMany, rxx, f64); +two_qubit_param_loop_execute!(RYYMany, ryy, f64); +two_qubit_param_loop_execute!(RZZMany, rzz, f64); +two_qubit_param_loop_execute!(Depolarize2Many, depolarize2, f64); +two_qubit_param_loop_execute!(CorrelatedLossMany, correlated_loss_channel, [f64; 3]); +two_qubit_param_loop_execute!(TwoQubitPauliErrorMany, two_qubit_pauli_error, [f64; 15]); + +impl CircuitOp for circuit::U3Many { + type Effect = (); + type Message = QubitBatch<[f64; 3]>; + + fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { + let [theta, phi, lambda] = msg.params; + let state = backend.state(); + for &q in &msg.qubits { + state.u3(q, theta, phi, lambda); + } + Ok(()) + } +} + +impl CircuitOp for circuit::MeasureMany { + type Effect = Effects; + type Message = QubitBatch; + + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result { + let mut measurement_results = MeasurementResult::with_capacity(msg.qubits.len()); + for &q in &msg.qubits { + measurement_results.push(backend.measure(q)?); + } + Ok(Effects::one(CircuitOutcomeEffect::Measurement( + MeasurementEffect { + measurement_results, + }, + ))) + } +} impl CircuitOp for circuit::U3 { type Effect = (); diff --git a/crates/vihaco-circuit-isa/src/messages.rs b/crates/vihaco-circuit-isa/src/messages.rs index 497b90f98..a26e23535 100644 --- a/crates/vihaco-circuit-isa/src/messages.rs +++ b/crates/vihaco-circuit-isa/src/messages.rs @@ -9,7 +9,7 @@ use vihaco::word::Word; /// Qubit batch plus a fixed parameter block; `P = ()` for plain batches. pub struct QubitBatch

{ - pub qubits: SmallVec<[u64; 8]>, + pub qubits: SmallVec<[usize; 8]>, pub params: P, } @@ -63,7 +63,8 @@ where let n = usize::try_from(PopStack::::pop(stack)?)?; let mut qubits = SmallVec::with_capacity(n); for _ in 0..n { - qubits.push(PopStack::::pop(stack)?); + let q = usize::try_from(PopStack::::pop(stack)?)?; + qubits.push(q); } qubits.reverse(); Ok(QubitBatch { qubits, params }) From 77404d1d1be0ad3212d8a0e1d8f86a16660861a4 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 15:24:33 +0100 Subject: [PATCH 20/36] Add BuildMessage impls -- this fails --- crates/ppvm-vihaco/src/component/mod.rs | 3 ++- crates/ppvm-vihaco/src/component/ops.rs | 26 +++++----------------- crates/ppvm-vihaco/src/composite.rs | 22 ++++++++++++++++++ crates/vihaco-circuit-isa/src/messages.rs | 27 +++++++++++++++++++++++ 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/crates/ppvm-vihaco/src/component/mod.rs b/crates/ppvm-vihaco/src/component/mod.rs index d5ef7e24c..e6d3ab315 100644 --- a/crates/ppvm-vihaco/src/component/mod.rs +++ b/crates/ppvm-vihaco/src/component/mod.rs @@ -10,7 +10,8 @@ pub use backend::{CircuitBackend, MAX_QUBITS, PauliSumBackend}; pub use executors::{Circuit, CircuitExecutor, TableauCircuit}; pub use ops::CircuitOp; pub use vihaco_circuit_isa::messages::{ - PopParams, QubitBatch, TraceMessage, TwoQubitPauliErrorMsg, pop_qubit_batch, + PopParams, QubitBatch, QubitPairBatch, TraceMessage, TwoQubitPauliErrorMsg, pop_qubit_batch, + pop_qubit_pair_batch, }; // vihaco resolves `#[device]` children through these generated companions. diff --git a/crates/ppvm-vihaco/src/component/ops.rs b/crates/ppvm-vihaco/src/component/ops.rs index c265954af..4cf5e4806 100644 --- a/crates/ppvm-vihaco/src/component/ops.rs +++ b/crates/ppvm-vihaco/src/component/ops.rs @@ -4,10 +4,9 @@ use super::backend::CircuitBackend; use crate::measurements::{CircuitOutcomeEffect, MeasurementEffect, MeasurementResult}; use ppvm_tableau::prelude::*; -use smallvec::SmallVec; use vihaco::{Effects, PopFromStack}; use vihaco_circuit_isa::circuit; -use vihaco_circuit_isa::messages::QubitBatch; +use vihaco_circuit_isa::messages::{QubitBatch, QubitPairBatch}; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { @@ -179,16 +178,6 @@ single_qubit_batch_execute!(SqrtYMany, sqrt_y_many); single_qubit_batch_execute!(SqrtYAdjMany, sqrt_y_dag_many); single_qubit_batch_execute!(ResetMany, reset_many); -/// Split a flat batch `[a0, b0, a1, b1, ..]` into `(a, b)` pairs. -fn pairs_of(qubits: &[usize]) -> eyre::Result> { - eyre::ensure!( - qubits.len() % 2 == 0, - "two-qubit batch needs an even number of qubits, got {}", - qubits.len() - ); - Ok(qubits.chunks_exact(2)) -} - macro_rules! single_qubit_rotation_batch_execute { ($leaf:ident, $method:ident) => { impl CircuitOp for circuit::$leaf { @@ -233,17 +222,14 @@ macro_rules! two_qubit_batch_execute { ($leaf:ident, $method:ident) => { impl CircuitOp for circuit::$leaf { type Effect = (); - type Message = QubitBatch; + type Message = QubitPairBatch; fn apply( &self, backend: &mut B, msg: Self::Message, ) -> eyre::Result<()> { - // `*_many` takes tuples, so the flat batch is copied into pairs. - let pairs: SmallVec<[(usize, usize); 4]> = - pairs_of(&msg.qubits)?.map(|p| (p[0], p[1])).collect(); - backend.state().$method(&pairs); + backend.state().$method(&msg.pairs); Ok(()) } } @@ -255,7 +241,7 @@ macro_rules! two_qubit_param_loop_execute { ($leaf:ident, $method:ident, $params:ty) => { impl CircuitOp for circuit::$leaf { type Effect = (); - type Message = QubitBatch<$params>; + type Message = QubitPairBatch<$params>; fn apply( &self, @@ -263,8 +249,8 @@ macro_rules! two_qubit_param_loop_execute { msg: Self::Message, ) -> eyre::Result<()> { let state = backend.state(); - for p in pairs_of(&msg.qubits)? { - state.$method(p[0], p[1], msg.params); + for &(a, b) in &msg.pairs { + state.$method(a, b, msg.params); } Ok(()) } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 756cfd295..3f4c9af8e 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -19,6 +19,9 @@ use crate::component::{Circuit, CircuitBackend, CircuitExecutor, CircuitOp}; use crate::measurements::{CircuitOutcomeEffect, MeasurementObserver, TraceObserver}; use vihaco_circuit_isa::circuit; +use vihaco_circuit_isa::messages::{ + PopParams, QubitBatch, QubitPairBatch, pop_qubit_batch, pop_qubit_pair_batch, +}; // Device configuration lives in `device_info` (shipped with the circuit // component so it compiles without the composite). Re-exported here so the @@ -48,6 +51,25 @@ type Instruction = ::Runtime; type Program = LocalModule; +// Batched circuit instructions pop their operands through custom messages. +impl BuildMessage> for PPVM +where + Circuit: Execute>, +{ + fn build_message(&mut self, _: &I, _: &Program) -> eyre::Result> { + pop_qubit_batch(&mut self.execution) + } +} + +impl BuildMessage> for PPVM +where + Circuit: Execute>, +{ + fn build_message(&mut self, _: &I, _: &Program) -> eyre::Result> { + pop_qubit_pair_batch(&mut self.execution) + } +} + // execution stepping impl Step for PPVM where diff --git a/crates/vihaco-circuit-isa/src/messages.rs b/crates/vihaco-circuit-isa/src/messages.rs index a26e23535..a094e4bdc 100644 --- a/crates/vihaco-circuit-isa/src/messages.rs +++ b/crates/vihaco-circuit-isa/src/messages.rs @@ -13,6 +13,12 @@ pub struct QubitBatch

{ pub params: P, } +/// Batch of qubit pairs plus a fixed parameter block; `P = ()` for plain batches. +pub struct QubitPairBatch

{ + pub pairs: SmallVec<[(usize, usize); 4]>, + pub params: P, +} + pub struct TwoQubitPauliErrorMsg { pub q0: u64, pub q1: u64, @@ -69,3 +75,24 @@ where qubits.reverse(); Ok(QubitBatch { qubits, params }) } + +/// Pop a [`QubitPairBatch`] pushed as `a0 b0 .. a(n-1) b(n-1)`, then the pair +/// count `n`, then the params. +/// +/// Call this from a composite's `BuildMessage` impl for batched instructions. +pub fn pop_qubit_pair_batch(stack: &mut S) -> eyre::Result> +where + S: PopStack + PopStack, + P: PopParams, +{ + let params = P::pop_params(stack)?; + let n = usize::try_from(PopStack::::pop(stack)?)?; + let mut pairs = SmallVec::with_capacity(n); + for _ in 0..n { + let b = usize::try_from(PopStack::::pop(stack)?)?; + let a = usize::try_from(PopStack::::pop(stack)?)?; + pairs.push((a, b)); + } + pairs.reverse(); + Ok(QubitPairBatch { pairs, params }) +} From 08a1c1ac3a002d638a5834c9013f854a7aed4b57 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 15:35:36 +0100 Subject: [PATCH 21/36] Work around build messages issue --- crates/ppvm-vihaco/src/composite.rs | 34 ++++++++++++----------- crates/vihaco-circuit-isa/src/messages.rs | 26 ++++++++++++++++- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 3f4c9af8e..64b2345b0 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -52,23 +52,25 @@ type Instruction = ::Runtime; type Program = LocalModule; // Batched circuit instructions pop their operands through custom messages. -impl BuildMessage> for PPVM -where - Circuit: Execute>, -{ - fn build_message(&mut self, _: &I, _: &Program) -> eyre::Result> { - pop_qubit_batch(&mut self.execution) - } -} +// TODO: per-composite builders conflict with vihaco's blanket `BuildMessage` (E0119) +// since the messages are foreign here; the ISA crate implements them for now instead. +// impl BuildMessage> for PPVM +// where +// Circuit: Execute>, +// { +// fn build_message(&mut self, _: &I, _: &Program) -> eyre::Result> { +// pop_qubit_batch(&mut self.execution) +// } +// } -impl BuildMessage> for PPVM -where - Circuit: Execute>, -{ - fn build_message(&mut self, _: &I, _: &Program) -> eyre::Result> { - pop_qubit_pair_batch(&mut self.execution) - } -} +// impl BuildMessage> for PPVM +// where +// Circuit: Execute>, +// { +// fn build_message(&mut self, _: &I, _: &Program) -> eyre::Result> { +// pop_qubit_pair_batch(&mut self.execution) +// } +// } // execution stepping impl Step for PPVM diff --git a/crates/vihaco-circuit-isa/src/messages.rs b/crates/vihaco-circuit-isa/src/messages.rs index a094e4bdc..5af9ba537 100644 --- a/crates/vihaco-circuit-isa/src/messages.rs +++ b/crates/vihaco-circuit-isa/src/messages.rs @@ -4,8 +4,9 @@ //! Custom circuit messages that don't fit vihaco's standard stack protocols. use smallvec::SmallVec; -use vihaco::PopStack; +use vihaco::runtime::{AutomaticMessage, PrepareMessage}; use vihaco::word::Word; +use vihaco::{Composite, Execute, Field, HasOperandStack, PopStack}; /// Qubit batch plus a fixed parameter block; `P = ()` for plain batches. pub struct QubitBatch

{ @@ -96,3 +97,26 @@ where pairs.reverse(); Ok(QubitPairBatch { pairs, params }) } + +// TODO: these use vihaco's `#[doc(hidden)]` message traits so the batches build in +// any composite; switch to a public extension point once QuEraComputing/vihaco#169 lands. +macro_rules! automatic_batch_message { + ($msg:ident, $pop:ident) => { + impl

AutomaticMessage for $msg

{} + + impl PrepareMessage for $msg

+ where + C: Composite + Field + HasOperandStack, + C::Child: Execute>, + C::Source: PopStack + PopStack, + P: PopParams, + { + fn prepare(parent: &mut C, _: &I, _: &Ctx) -> eyre::Result { + $pop(parent.operand_stack(1)) + } + } + }; +} + +automatic_batch_message!(QubitBatch, pop_qubit_batch); +automatic_batch_message!(QubitPairBatch, pop_qubit_pair_batch); From b9d338bb36f08b2dceba5d4fd7050632ccfadd06 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 15:44:22 +0100 Subject: [PATCH 22/36] Split out header and delete rest of obsolete syntax file --- Cargo.lock | 1 + crates/ppvm-vihaco/Cargo.toml | 1 + crates/ppvm-vihaco/src/header.rs | 77 +++++++ crates/ppvm-vihaco/src/lib.rs | 6 +- crates/ppvm-vihaco/src/syntax.rs | 355 ------------------------------- 5 files changed, 81 insertions(+), 359 deletions(-) create mode 100644 crates/ppvm-vihaco/src/header.rs delete mode 100644 crates/ppvm-vihaco/src/syntax.rs diff --git a/Cargo.lock b/Cargo.lock index 40f52f284..6ebc94c74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2409,6 +2409,7 @@ dependencies = [ "vihaco", "vihaco-circuit-isa", "vihaco-cpu", + "vihaco-parser", "vihaco-stdlib", ] diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index c942fcd65..350113e31 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -24,6 +24,7 @@ ppvm-tableau = { version = "0.1.1", path = "../ppvm-tableau" } smallvec = "1.15.1" vihaco = { version = "0.7.0", features = ["program-image-execution"] } vihaco-stdlib = { version = "0.7.0", features = ["program-image-execution"] } +vihaco-parser = "0.7.0" vihaco-circuit-isa = { version = "0.1.1", path = "../vihaco-circuit-isa" } ppvm-pauli-sum = { version = "0.1.1", path = "../ppvm-pauli-sum" } rand = "0.10.3" diff --git a/crates/ppvm-vihaco/src/header.rs b/crates/ppvm-vihaco/src/header.rs new file mode 100644 index 000000000..2259680e9 --- /dev/null +++ b/crates/ppvm-vihaco/src/header.rs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Declarations in a PPVM program's `.header(...)` section, collected into the +//! program metadata and a [`PPVMDeviceInfo`]. + +use crate::device_info::{BackendKind, PPVMDeviceInfo}; +use vihaco::module::ProgramMetadata; +use vihaco::{Parse, Type, Value}; +use vihaco_parser::Ident; + +pub type Metadata = ProgramMetadata; + +#[derive(Debug, Clone, PartialEq, Parse)] +#[syntax_class(value)] +pub enum BackendKindSyntax { + #[pattern = "`tableau`"] + Tableau, + #[pattern = "`paulisum`"] + PauliSum, + #[pattern = "`lossy_paulisum`"] + LossyPauliSum, +} + +impl From for BackendKind { + fn from(value: BackendKindSyntax) -> Self { + match value { + BackendKindSyntax::Tableau => Self::Tableau, + BackendKindSyntax::PauliSum => Self::PauliSum, + BackendKindSyntax::LossyPauliSum => Self::LossyPauliSum, + } + } +} + +/// One `;`-terminated declaration in a PPVM header section. +#[derive(Debug, Parse)] +#[syntax_class(metadata)] +pub enum HeaderDeclaration { + #[pattern = "$0"] + Metadata(Metadata), + #[pattern = "circuit.n_qubits $0"] + NumQubits(usize), + #[pattern = "circuit.coefficient_threshold $0"] + CoefficientThreshold(f64), + #[pattern = "circuit.backend $0"] + Backend(BackendKindSyntax), + #[pattern = "circuit.observable $0"] + Observable(Ident), + #[pattern = "circuit.max_pauli_weight $0"] + MaxPauliWeight(usize), +} + +/// Collect header declarations into the program metadata and device info. +/// +/// Exactly one metadata block is required. A repeated `circuit.*` declaration +/// overwrites the earlier one, as in the previous header syntax. +pub fn collect_header( + declarations: impl IntoIterator, +) -> eyre::Result<(Metadata, PPVMDeviceInfo)> { + let mut metadata = None; + let mut info = PPVMDeviceInfo::default(); + for declaration in declarations { + match declaration { + HeaderDeclaration::Metadata(m) => { + eyre::ensure!(metadata.is_none(), "duplicate metadata declaration"); + metadata = Some(m); + } + HeaderDeclaration::NumQubits(n) => info.n_qubits = n, + HeaderDeclaration::CoefficientThreshold(t) => info.coefficient_threshold = t, + HeaderDeclaration::Backend(b) => info.backend = b.into(), + HeaderDeclaration::Observable(s) => info.observable = Some(s.0), + HeaderDeclaration::MaxPauliWeight(w) => info.max_pauli_weight = Some(w), + } + } + let metadata = metadata.ok_or_else(|| eyre::eyre!("missing program metadata"))?; + Ok((metadata, info)) +} diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index c59a66b15..a60afc105 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -5,17 +5,15 @@ pub mod component; pub mod composite; pub mod device_info; +pub mod header; pub mod measurements; pub mod observable; // pub mod shots; -// mod syntax; /// Re-exported so consumers (e.g. the CLI REPL) can name gates for /// [`composite::PPVM::apply_circuit_instruction`] without depending on the ISA /// crate directly. -// pub use vihaco_circuit_isa::CircuitInstruction; -use vihaco::syntax::ParsedModule; -use vihaco::{Type, Value, module::LocalModule}; +pub use vihaco_circuit_isa::circuit; // use crate::composite::ppvm_module as ppvm; // use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs deleted file mode 100644 index 7af45af20..000000000 --- a/crates/ppvm-vihaco/src/syntax.rs +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The PPVM Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashMap; - -use chumsky::{IterParser, Parser}; -use vihaco::module::{FunctionInfo, LabelInfo, LocalModule, Parameter, Signature}; -use vihaco::syntax::{Param, ParsedFunction, ParsedModule, Resolve, skip}; -use vihaco::{Parse, Type, Value}; -use vihaco_circuit_isa::{CircuitInstruction, CircuitSurfaceInstruction}; -use vihaco_cpu::{RuntimeInstruction as CpuRuntime, SurfaceInstruction as CpuSurface}; -use vihaco_cpu::{SurfaceType, SurfaceValue}; -use vihaco_parser::{BareToken, Ident, QuotedString}; - -use crate::composite::ppvm_module as ppvm; -use crate::composite::{BackendKind, PPVMDeviceInfo, PPVMInstruction}; - -#[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] -#[syntax_class(value)] -pub enum BackendKindSyntax { - #[pattern = "`tableau`"] - Tableau, - #[pattern = "`paulisum`"] - PauliSum, - #[pattern = "`lossy_paulisum`"] - LossyPauliSum, -} - -impl From for BackendKind { - fn from(value: BackendKindSyntax) -> Self { - match value { - BackendKindSyntax::Tableau => Self::Tableau, - BackendKindSyntax::PauliSum => Self::PauliSum, - BackendKindSyntax::LossyPauliSum => Self::LossyPauliSum, - } - } -} - -#[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] -#[syntax_class(metadata, head = "device")] -pub enum PPVMHeader { - #[pattern = "circuit.n_qubits $0"] - NumQubits(usize), - #[pattern = "circuit.coefficient_threshold $0"] - CoefficientThreshold(f64), - #[pattern = "circuit.backend $0"] - Backend(BackendKindSyntax), - #[pattern = "circuit.observable $0"] - Observable(Ident), - #[pattern = "circuit.max_pauli_weight $0"] - MaxPauliWeight(usize), -} - -#[derive(Debug, Default)] -pub struct PPVMResolver { - strings: Vec, -} - -impl PPVMResolver { - pub fn new() -> Self { - Self::default() - } - - fn apply_header(info: &mut PPVMDeviceInfo, header: PPVMHeader) -> eyre::Result<()> { - match header { - PPVMHeader::NumQubits(n) => info.n_qubits = n, - PPVMHeader::CoefficientThreshold(t) => info.coefficient_threshold = t, - PPVMHeader::Backend(b) => info.backend = b.into(), - PPVMHeader::Observable(s) => info.observable = Some(s.as_str().to_owned()), - PPVMHeader::MaxPauliWeight(w) => info.max_pauli_weight = Some(w), - } - Ok(()) - } - - fn intern(&mut self, value: &str) -> u32 { - if let Some(index) = self.strings.iter().position(|item| item == value) { - return index as u32; - } - let index = self.strings.len() as u32; - self.strings.push(value.to_owned()); - index - } - - fn runtime_type(ty: SurfaceType) -> Type { - match ty { - SurfaceType::Undefined => Type::Undefined, - SurfaceType::String => Type::String, - SurfaceType::Bool => Type::Bool, - SurfaceType::I64 => Type::I64, - SurfaceType::U32 => Type::U32, - SurfaceType::U64 => Type::U64, - SurfaceType::F64 => Type::F64, - SurfaceType::FunctionRef => Type::FunctionRef, - SurfaceType::HeapRef => Type::HeapRef, - } - } - - fn lower_value(&mut self, ty: SurfaceType, value: SurfaceValue) -> eyre::Result { - let text = match value { - SurfaceValue::Quoted(QuotedString(value)) => { - return Ok(Value::String(self.intern(&value))); - } - SurfaceValue::Bare(BareToken(value)) => value, - }; - Ok(match ty { - SurfaceType::Undefined => Value::Undefined, - SurfaceType::String => Value::String(self.intern(&text)), - SurfaceType::Bool => Value::Bool(text.parse()?), - SurfaceType::I64 => Value::I64(text.parse()?), - SurfaceType::U32 => Value::U32(text.parse()?), - SurfaceType::U64 => Value::U64(text.parse()?), - SurfaceType::F64 => Value::F64(text.parse()?), - SurfaceType::FunctionRef => Value::FunctionRef(text.parse()?), - SurfaceType::HeapRef => Value::HeapRef(text.parse()?), - }) - } - - fn lower_cpu( - &mut self, - instruction: CpuSurface, - labels: &HashMap, - functions: &HashMap, - ) -> eyre::Result> { - use CpuRuntime as R; - use CpuSurface as S; - let runtime = match instruction { - S::Span(a, b, c) => R::Span(a, b, c), - S::Label(_) => return Ok(None), - S::FunctionStart => R::FunctionStart, - S::FunctionEnd => R::FunctionEnd, - S::Breakpoint => R::Breakpoint, - S::Branch(name) => R::Branch( - *labels - .get(name.as_str()) - .ok_or_else(|| eyre::eyre!("undefined label `@{}`", name.as_str()))?, - ), - S::ConditionalBranch(a, b) => R::ConditionalBranch( - *labels - .get(a.as_str()) - .ok_or_else(|| eyre::eyre!("undefined label `@{}`", a.as_str()))?, - *labels - .get(b.as_str()) - .ok_or_else(|| eyre::eyre!("undefined label `@{}`", b.as_str()))?, - ), - S::Return(n) => R::Return(n), - S::IndirectCall => R::IndirectCall, - S::Call(arity, name) => R::Call( - arity, - *functions - .get(name.as_str()) - .ok_or_else(|| eyre::eyre!("undefined function `@{}`", name.as_str()))?, - ), - S::Halt => R::Halt, - S::Print => R::Print, - S::Load(ty, slot) => R::Load(Self::runtime_type(ty), slot), - S::Store(ty, slot) => R::Store(Self::runtime_type(ty), slot), - S::Dup => R::Dup, - S::HeapAlloc(n) => R::HeapAlloc(n), - S::GetItem => R::GetItem, - S::HeapDealloc => R::HeapDealloc, - S::Const(ty, value) => { - let value = self.lower_value(ty, value)?; - R::Const(Self::runtime_type(ty), value) - } - S::Add(ty) => R::Add(Self::runtime_type(ty)), - S::Sub(ty) => R::Sub(Self::runtime_type(ty)), - S::Mul(ty) => R::Mul(Self::runtime_type(ty)), - S::Div(ty) => R::Div(Self::runtime_type(ty)), - S::Rem(ty) => R::Rem(Self::runtime_type(ty)), - S::Neg(ty) => R::Neg(Self::runtime_type(ty)), - S::Shl(ty) => R::Shl(Self::runtime_type(ty)), - S::Shr(ty) => R::Shr(Self::runtime_type(ty)), - S::Rol(ty) => R::Rol(Self::runtime_type(ty)), - S::Ror(ty) => R::Ror(Self::runtime_type(ty)), - S::BitAnd(ty) => R::BitAnd(Self::runtime_type(ty)), - S::BitOr(ty) => R::BitOr(Self::runtime_type(ty)), - S::BitXor(ty) => R::BitXor(Self::runtime_type(ty)), - S::Not => R::Not, - S::And => R::And, - S::Or => R::Or, - S::Xor => R::Xor, - S::Eq(ty) => R::Eq(Self::runtime_type(ty)), - S::Ne(ty) => R::Ne(Self::runtime_type(ty)), - S::Lt(ty) => R::Lt(Self::runtime_type(ty)), - S::Gt(ty) => R::Gt(Self::runtime_type(ty)), - S::Le(ty) => R::Le(Self::runtime_type(ty)), - S::Ge(ty) => R::Ge(Self::runtime_type(ty)), - }; - Ok(Some(runtime)) - } - - fn lower_circuit(instruction: CircuitSurfaceInstruction) -> CircuitInstruction { - use CircuitInstruction as R; - use CircuitSurfaceInstruction as S; - match instruction { - S::TwoQubitPauliError => R::TwoQubitPauliError, - S::Truncate => R::Truncate, - S::Trace => R::Trace, - S::X => R::X, - S::Y => R::Y, - S::Z => R::Z, - S::H => R::H, - S::SqrtXAdj => R::SqrtXAdj, - S::SqrtX => R::SqrtX, - S::SqrtYAdj => R::SqrtYAdj, - S::SqrtY => R::SqrtY, - S::SAdj => R::SAdj, - S::S => R::S, - S::CNOT => R::CNOT, - S::CZ => R::CZ, - S::TAdj => R::TAdj, - S::T => R::T, - S::RXX => R::RXX, - S::RYY => R::RYY, - S::RZZ => R::RZZ, - S::RX => R::RX, - S::RY => R::RY, - S::RZ => R::RZ, - S::U3 => R::U3, - S::Measure => R::Measure, - S::Reset => R::Reset, - S::R => R::R, - S::Loss => R::Loss, - S::CorrelatedLoss => R::CorrelatedLoss, - S::PauliError => R::PauliError, - S::Depolarize2 => R::Depolarize2, - S::Depolarize => R::Depolarize, - S::Leakage => R::Leakage, - } - } -} - -impl Resolve> for PPVMResolver { - type Module = LocalModule; - - fn resolve_module( - &mut self, - parsed: ParsedModule>, - ) -> eyre::Result { - let mut module = LocalModule::default(); - for header in parsed.header { - Self::apply_header(&mut module.extra, header)?; - } - - let mut functions = HashMap::new(); - let mut function_address = 0u32; - for function in &parsed.functions { - functions.insert(function.name.as_str().to_owned(), function_address); - function_address += function - .body - .iter() - .filter(|instruction| { - !matches!( - instruction, - ppvm::syntax::Instruction::Cpu(CpuSurface::Label(_)) - ) - }) - .count() as u32; - } - - let mut labels = HashMap::new(); - let mut address = 0u32; - for function in &parsed.functions { - for instruction in &function.body { - if let ppvm::syntax::Instruction::Cpu(CpuSurface::Label(name)) = instruction { - labels.insert(name.as_str().to_owned(), address); - module.labels.push(LabelInfo { - address, - name: self.intern(name.as_str()), - }); - } else { - address += 1; - } - } - } - - for function in parsed.functions { - let start_address = module.code.len() as u32; - for instruction in function.body { - let runtime = match instruction { - ppvm::syntax::Instruction::Cpu(cpu) => self - .lower_cpu(cpu, &labels, &functions)? - .map(PPVMInstruction::Cpu), - ppvm::syntax::Instruction::Circuit(circuit) => { - Some(PPVMInstruction::Circuit(Self::lower_circuit(circuit))) - } - }; - if let Some(runtime) = runtime { - module.code.push(runtime); - } - } - let end_address = module.code.len() as u32; - module.functions.push(FunctionInfo { - name: self.intern(function.name.as_str()), - signature: Signature { - params: function - .params - .into_iter() - .map(|Param { name, ty }| Parameter { - name: self.intern(name.as_str()), - ty: Self::runtime_type(ty), - }) - .collect(), - ret: function - .return_ty - .map(Self::runtime_type) - .into_iter() - .collect(), - }, - local_count: 0, - start_address, - end_address, - file: 0, - }); - } - module.main_function = functions.get("main").copied(); - module.strings = std::mem::take(&mut self.strings); - Ok(module) - } -} - -pub fn parse_headers(source: &str) -> eyre::Result<(Vec, String)> { - let mut headers = Vec::new(); - let mut body = String::new(); - for line in source.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("device ") { - let header = trimmed.strip_suffix(';').unwrap_or(trimmed); - let parsed = PPVMHeader::parser() - .parse(header) - .into_result() - .map_err(|errors| eyre::eyre!("invalid device header `{header}`: {errors:?}"))?; - headers.push(parsed); - } else { - body.push_str(line); - body.push('\n'); - } - } - Ok((headers, body)) -} - -pub fn parse_functions( - source: &str, -) -> eyre::Result>> { - skip() - .ignore_then( - ParsedFunction::::parser() - .repeated() - .collect::>(), - ) - .then_ignore(skip()) - .parse(source) - .into_result() - .map_err(|errors| eyre::eyre!("parsing functions failed: {errors:?}")) -} From 546f671fe6f4b8c55f292de3e9bb3405cef1af63 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 15:46:47 +0100 Subject: [PATCH 23/36] Delete some old stuff --- crates/ppvm-vihaco/src/composite.rs | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 64b2345b0..6ad5992b1 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1,26 +1,12 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -use eyre::{Result, eyre}; -use ppvm_pauli_sum::config::fx64hash::Byte8F64; -use ppvm_tableau::data::GeneralizedTableau; -use vihaco::PushToStack; -use vihaco::frame::Frame; -use vihaco::machine::StackFrame; -use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; -use vihaco::traits::{GetProgramInfo, ProgramCounter, StackMemory}; -use vihaco::{ - BuildMessage, Effects, Execute, ExecutionState, HasInstructionSet, NoContext, Observe, - ProgramImage, Step, Type, Value, composite, field, module::LocalModule, observe, program, -}; -use vihaco_cpu::CPU; - -use crate::component::{Circuit, CircuitBackend, CircuitExecutor, CircuitOp}; +use crate::component::Circuit; use crate::measurements::{CircuitOutcomeEffect, MeasurementObserver, TraceObserver}; - -use vihaco_circuit_isa::circuit; -use vihaco_circuit_isa::messages::{ - PopParams, QubitBatch, QubitPairBatch, pop_qubit_batch, pop_qubit_pair_batch, +use eyre; +use vihaco::{ + BuildMessage, Effects, Execute, ExecutionState, HasInstructionSet, Step, Type, Value, + composite, field, module::LocalModule, }; // Device configuration lives in `device_info` (shipped with the circuit From 1ce165e58624227b43d46326a5366ce800690832 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 15:59:25 +0100 Subject: [PATCH 24/36] Implement parsing and loading --- crates/ppvm-vihaco/src/component/executors.rs | 26 ++- crates/ppvm-vihaco/src/composite.rs | 40 +++- crates/ppvm-vihaco/src/lib.rs | 6 +- crates/ppvm-vihaco/src/loader.rs | 99 +++++++++ crates/ppvm-vihaco/src/shots.rs | 202 +++++++++--------- 5 files changed, 266 insertions(+), 107 deletions(-) create mode 100644 crates/ppvm-vihaco/src/loader.rs diff --git a/crates/ppvm-vihaco/src/component/executors.rs b/crates/ppvm-vihaco/src/component/executors.rs index 8d9355445..2a51b31a6 100644 --- a/crates/ppvm-vihaco/src/component/executors.rs +++ b/crates/ppvm-vihaco/src/component/executors.rs @@ -4,8 +4,8 @@ use super::backend::CircuitBackend; use super::buckets::{CircuitKind, LossyPauliSumBuckets, PauliSumBuckets, TableauBuckets}; use super::ops::CircuitOp; -use crate::device_info::PPVMDeviceInfo; -use eyre::Result; +use crate::device_info::{BackendKind, PPVMDeviceInfo}; +use eyre::{Result, eyre}; use vihaco::{Execute, component}; use vihaco_circuit_isa::circuit; @@ -92,6 +92,17 @@ impl vihaco::Reset for TableauCircuit { } impl Circuit { + /// Build the backend selected by `info.backend`. `seed` makes tableau runs + /// reproducible; the PauliSum backends are deterministic and ignore it. + pub fn new(info: &PPVMDeviceInfo, seed: Option) -> Result { + match (info.backend, seed) { + (BackendKind::Tableau, None) => Self::tableau(info), + (BackendKind::Tableau, Some(seed)) => Self::tableau_with_seed(info, seed), + (BackendKind::PauliSum, _) => Self::paulisum(info, &observable_terms(info)?), + (BackendKind::LossyPauliSum, _) => Self::lossy_paulisum(info, &observable_terms(info)?), + } + } + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and /// `coefficient_threshold` from `info`; no observable required. pub fn tableau(info: &PPVMDeviceInfo) -> Result { @@ -160,3 +171,14 @@ impl Default for TableauCircuit { Self::new(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") } } + +/// The PauliSum backends' initial observable, parsed from `circuit.observable`. +fn observable_terms(info: &PPVMDeviceInfo) -> Result> { + let observable = info.observable.as_deref().ok_or_else(|| { + eyre!( + "the {:?} backend requires `circuit.observable` to be set", + info.backend + ) + })?; + crate::observable::parse_pauli_sum_terms(observable, info.n_qubits) +} diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 6ad5992b1..1a130f07f 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::component::Circuit; -use crate::measurements::{CircuitOutcomeEffect, MeasurementObserver, TraceObserver}; +use crate::measurements::{ + CircuitOutcomeEffect, MeasurementObserver, MeasurementResult, TraceObserver, +}; use eyre; use vihaco::{ BuildMessage, Effects, Execute, ExecutionState, HasInstructionSet, Step, Type, Value, @@ -14,7 +16,7 @@ use vihaco::{ // existing `crate::composite::{…}` paths keep resolving. pub use crate::device_info::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo}; -// #[derive(Default)] +#[derive(Default)] #[composite] pub struct PPVM { #[device(0)] @@ -35,7 +37,39 @@ pub struct PPVM { type Instruction = ::Runtime; -type Program = LocalModule; +/// A lowered PPVM program; `extra` carries the header's device configuration. +pub type PPVMModule = LocalModule; + +type Program = PPVMModule; + +impl PPVM { + /// A fresh machine for the device described by `info`, ready to execute. + pub fn new(info: &PPVMDeviceInfo, seed: Option) -> eyre::Result { + if info.n_qubits == 0 { + eyre::bail!("circuit.n_qubits must be declared"); + } + Ok(Self { + execution: ExecutionState::new(), + cpu: vihaco_cpu::CPU::default(), + circuit: Circuit::new(info, seed)?, + measurement_record: MeasurementObserver::default(), + trace_record: TraceObserver::default(), + }) + } + + pub fn measurement_record(&self) -> &[MeasurementResult] { + &self.measurement_record.record + } + + pub fn trace_record(&self) -> &[f64] { + &self.trace_record.record + } + + /// Consume the machine, returning its measurement and trace records. + pub fn into_records(self) -> (Vec, Vec) { + (self.measurement_record.record, self.trace_record.record) + } +} // Batched circuit instructions pop their operands through custom messages. // TODO: per-composite builders conflict with vihaco's blanket `BuildMessage` (E0119) diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index a60afc105..42d7c08ee 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -6,15 +6,19 @@ pub mod component; pub mod composite; pub mod device_info; pub mod header; +pub mod loader; pub mod measurements; pub mod observable; -// pub mod shots; +pub mod shots; /// Re-exported so consumers (e.g. the CLI REPL) can name gates for /// [`composite::PPVM::apply_circuit_instruction`] without depending on the ISA /// crate directly. pub use vihaco_circuit_isa::circuit; +pub use composite::PPVMModule; +pub use loader::{compile_program, load_module_file}; + // use crate::composite::ppvm_module as ppvm; // use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; // use crate::syntax::{PPVMHeader, PPVMResolver, parse_functions, parse_headers}; diff --git a/crates/ppvm-vihaco/src/loader.rs b/crates/ppvm-vihaco/src/loader.rs new file mode 100644 index 000000000..de20011b6 --- /dev/null +++ b/crates/ppvm-vihaco/src/loader.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Turning PPVM program source into a [`PPVMModule`] ready to execute. + +use crate::composite::{PPVM, PPVMModule}; +use crate::header::{HeaderDeclaration, collect_header}; +use vihaco::loader::MetadataLoweringContext; +use vihaco::syntax::{HeaderDeclarations, ParsedModule, lower_functions_with_metadata}; +use vihaco::{HasInstructionSet, NoContext, SstFile, Type}; + +/// Name of the SST section holding the PPVM program. +pub const PPVM_SECTION: &str = "ppvm"; + +type SourceInstruction = ::Syntax; + +/// Parse and lower SST source into a module whose `extra` is the device info. +pub fn compile_program(source: &str) -> eyre::Result { + let file = SstFile::::from_text(source)?; + let section = file + .root() + .child(PPVM_SECTION) + .ok_or_else(|| eyre::eyre!("missing `{PPVM_SECTION}` section"))?; + + let ParsedModule { header, functions } = ParsedModule::< + SourceInstruction, + Type, + HeaderDeclarations, + >::parse_section(section)?; + let (metadata, info) = collect_header(header)?; + + let mut context = MetadataLoweringContext::new(&metadata)?; + let code = lower_functions_with_metadata(functions, &metadata, &mut context)?; + metadata.map_extra(|_| info).into_module(code) +} + +/// Read a file and compile it with [`compile_program`]. +pub fn load_module_file(path: &str) -> eyre::Result { + compile_program(&std::fs::read_to_string(path)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::measurements::MeasurementOutcome; + + /// X on |0> then measure: the outcome is deterministically 1. + const FLIP_AND_MEASURE: &str = r#"sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "flip.sst"], + functions: [ + { + name: 0, + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 5, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + execution::program::const_u64 0 + circuit::circuit::x + execution::program::const_u64 0 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). +"#; + + #[test] + fn compiled_program_runs_on_the_declared_device() -> eyre::Result<()> { + let module = compile_program(FLIP_AND_MEASURE)?; + assert_eq!(module.extra.n_qubits, 1); + + let mut machine = PPVM::new(&module.extra, Some(0))?; + vihaco::execute(&module, &mut machine, &[])?; + assert_eq!(machine.measurement_record().len(), 1); + assert_eq!( + machine.measurement_record()[0].as_slice(), + [MeasurementOutcome::One] + ); + Ok(()) + } +} diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs index 0c31d1a4e..da0cecdcc 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -4,13 +4,12 @@ //! Running a compiled program for many shots, optionally across threads. //! //! Each shot runs on a fresh [`PPVM`] so shots are fully independent; the -//! module is compiled once and cloned into each shot's machine. With the `rayon` feature, +//! module is compiled once and borrowed by each shot's machine. With the `rayon` feature, //! [`run_shots`] parallelizes across shots when the global pool (sized once by //! [`set_global_threads`]) has more than one thread and there are enough shots //! to amortize the overhead. -use crate::PPVMModule; -use crate::composite::PPVM; +use crate::composite::{PPVM, PPVMModule}; use crate::measurements::MeasurementResult; use rand::{RngExt, SeedableRng, rngs::SmallRng}; @@ -41,12 +40,13 @@ fn shot_seed(base: Option, index: usize) -> Option { /// Run a single shot on a fresh machine and return both records. fn run_one_shot(module: &PPVMModule, seed: Option) -> eyre::Result { - let mut machine = PPVM::default(); - machine.load(module)?; - machine.run_with_seed(seed)?; + let mut machine = PPVM::new(&module.extra, seed)?; + // The module is only borrowed, so all shots share one compiled program. + vihaco::execute(module, &mut machine, &[])?; + let (measurements, traces) = machine.into_records(); Ok(ShotRecord { - measurements: machine.measurement_record(), - traces: machine.trace_record(), + measurements, + traces, }) } @@ -126,96 +126,96 @@ pub fn set_global_threads(threads: usize) -> eyre::Result<()> { Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - use crate::compile_program; - use crate::measurements::MeasurementOutcome; - - /// Measures q0 in |0>: every shot is deterministically `0`. - const DETERMINISTIC: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; - - /// Prepares |+> with H, then measures q0: each shot is a random 0/1. - const RANDOM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.h\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; - - fn module(src: &str) -> PPVMModule { - compile_program(src).unwrap() - } - - #[test] - fn serial_runs_one_record_per_shot() { - let m = module(DETERMINISTIC); - let records = run_shots_serial(&m, 5, None).unwrap(); - assert_eq!(records.len(), 5); - for shot in &records { - // One measurement event, one qubit, deterministically |0>. - assert_eq!(shot.measurements.len(), 1); - assert_eq!(shot.measurements[0].as_slice(), [MeasurementOutcome::Zero]); - assert!(shot.traces.is_empty(), "Tableau backend emits no traces"); - } - } - - #[test] - fn dispatcher_runs_all_shots() { - let m = module(DETERMINISTIC); - // 10 shots is below the parallel threshold, so this takes the serial path. - let records = run_shots(&m, 10, None).unwrap(); - assert_eq!(records.len(), 10); - } - - #[test] - fn same_seed_is_reproducible() { - let m = module(RANDOM); - let a = run_shots_serial(&m, 20, Some(42)).unwrap(); - let b = run_shots_serial(&m, 20, Some(42)).unwrap(); - assert_eq!(a, b); - } - - #[test] - fn per_shot_seeds_differ() { - // If every shot shared one seed, all 20 outcomes would be identical. - // Distinct per-shot seeds must produce a mix of 0s and 1s. - let m = module(RANDOM); - let records = run_shots_serial(&m, 20, Some(42)).unwrap(); - let first = &records[0]; - assert!( - records.iter().any(|r| r != first), - "expected varied outcomes across shots, got {records:?}" - ); - } - - #[test] - fn nearby_seeds_do_not_share_shots() { - // With `seed + i`, seed 8 was seed 7 shifted by one shot. - let m = module(RANDOM); - let a = run_shots_serial(&m, 128, Some(7)).unwrap(); - let b = run_shots_serial(&m, 128, Some(8)).unwrap(); - assert_ne!( - a[1..], - b[..127], - "seed 8 reproduced seed 7's shots shifted by one" - ); - } - - #[cfg(feature = "rayon")] - #[test] - fn serial_and_parallel_match_for_same_seed() { - let m = module(RANDOM); - let serial = run_shots_serial(&m, 64, Some(7)).unwrap(); - let parallel = run_shots_parallel(&m, 64, Some(7)).unwrap(); - assert_eq!(serial, parallel); - } - - #[cfg(feature = "rayon")] - #[test] - fn parallelizes_only_with_multiple_threads_above_threshold() { - // A single-thread pool is always serial, no matter how many shots. - assert!(!should_parallelize(1, 100_000)); - // Multiple threads, but too few shots to amortize the overhead: serial. - assert!(!should_parallelize(8, PARALLEL_SHOT_THRESHOLD - 1)); - // Multiple threads at the threshold: parallel. - assert!(should_parallelize(8, PARALLEL_SHOT_THRESHOLD)); - // Multiple threads, plenty of shots: parallel. - assert!(should_parallelize(8, 100_000)); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::compile_program; +// use crate::measurements::MeasurementOutcome; + +// /// Measures q0 in |0>: every shot is deterministically `0`. +// const DETERMINISTIC: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; + +// /// Prepares |+> with H, then measures q0: each shot is a random 0/1. +// const RANDOM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.h\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; + +// fn module(src: &str) -> PPVMModule { +// compile_program(src).unwrap() +// } + +// #[test] +// fn serial_runs_one_record_per_shot() { +// let m = module(DETERMINISTIC); +// let records = run_shots_serial(&m, 5, None).unwrap(); +// assert_eq!(records.len(), 5); +// for shot in &records { +// // One measurement event, one qubit, deterministically |0>. +// assert_eq!(shot.measurements.len(), 1); +// assert_eq!(shot.measurements[0].as_slice(), [MeasurementOutcome::Zero]); +// assert!(shot.traces.is_empty(), "Tableau backend emits no traces"); +// } +// } + +// #[test] +// fn dispatcher_runs_all_shots() { +// let m = module(DETERMINISTIC); +// // 10 shots is below the parallel threshold, so this takes the serial path. +// let records = run_shots(&m, 10, None).unwrap(); +// assert_eq!(records.len(), 10); +// } + +// #[test] +// fn same_seed_is_reproducible() { +// let m = module(RANDOM); +// let a = run_shots_serial(&m, 20, Some(42)).unwrap(); +// let b = run_shots_serial(&m, 20, Some(42)).unwrap(); +// assert_eq!(a, b); +// } + +// #[test] +// fn per_shot_seeds_differ() { +// // If every shot shared one seed, all 20 outcomes would be identical. +// // Distinct per-shot seeds must produce a mix of 0s and 1s. +// let m = module(RANDOM); +// let records = run_shots_serial(&m, 20, Some(42)).unwrap(); +// let first = &records[0]; +// assert!( +// records.iter().any(|r| r != first), +// "expected varied outcomes across shots, got {records:?}" +// ); +// } + +// #[test] +// fn nearby_seeds_do_not_share_shots() { +// // With `seed + i`, seed 8 was seed 7 shifted by one shot. +// let m = module(RANDOM); +// let a = run_shots_serial(&m, 128, Some(7)).unwrap(); +// let b = run_shots_serial(&m, 128, Some(8)).unwrap(); +// assert_ne!( +// a[1..], +// b[..127], +// "seed 8 reproduced seed 7's shots shifted by one" +// ); +// } + +// #[cfg(feature = "rayon")] +// #[test] +// fn serial_and_parallel_match_for_same_seed() { +// let m = module(RANDOM); +// let serial = run_shots_serial(&m, 64, Some(7)).unwrap(); +// let parallel = run_shots_parallel(&m, 64, Some(7)).unwrap(); +// assert_eq!(serial, parallel); +// } + +// #[cfg(feature = "rayon")] +// #[test] +// fn parallelizes_only_with_multiple_threads_above_threshold() { +// // A single-thread pool is always serial, no matter how many shots. +// assert!(!should_parallelize(1, 100_000)); +// // Multiple threads, but too few shots to amortize the overhead: serial. +// assert!(!should_parallelize(8, PARALLEL_SHOT_THRESHOLD - 1)); +// // Multiple threads at the threshold: parallel. +// assert!(should_parallelize(8, PARALLEL_SHOT_THRESHOLD)); +// // Multiple threads, plenty of shots: parallel. +// assert!(should_parallelize(8, 100_000)); +// } +// } From 47032e24fc4891331109acf624e2e7b1002d95d2 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 16:01:24 +0100 Subject: [PATCH 25/36] Port shots.rs tests --- crates/ppvm-vihaco/src/shots.rs | 233 +++++++++++++++++++------------- 1 file changed, 140 insertions(+), 93 deletions(-) diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs index da0cecdcc..a7850bc10 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -126,96 +126,143 @@ pub fn set_global_threads(threads: usize) -> eyre::Result<()> { Ok(()) } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::compile_program; -// use crate::measurements::MeasurementOutcome; - -// /// Measures q0 in |0>: every shot is deterministically `0`. -// const DETERMINISTIC: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; - -// /// Prepares |+> with H, then measures q0: each shot is a random 0/1. -// const RANDOM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.h\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; - -// fn module(src: &str) -> PPVMModule { -// compile_program(src).unwrap() -// } - -// #[test] -// fn serial_runs_one_record_per_shot() { -// let m = module(DETERMINISTIC); -// let records = run_shots_serial(&m, 5, None).unwrap(); -// assert_eq!(records.len(), 5); -// for shot in &records { -// // One measurement event, one qubit, deterministically |0>. -// assert_eq!(shot.measurements.len(), 1); -// assert_eq!(shot.measurements[0].as_slice(), [MeasurementOutcome::Zero]); -// assert!(shot.traces.is_empty(), "Tableau backend emits no traces"); -// } -// } - -// #[test] -// fn dispatcher_runs_all_shots() { -// let m = module(DETERMINISTIC); -// // 10 shots is below the parallel threshold, so this takes the serial path. -// let records = run_shots(&m, 10, None).unwrap(); -// assert_eq!(records.len(), 10); -// } - -// #[test] -// fn same_seed_is_reproducible() { -// let m = module(RANDOM); -// let a = run_shots_serial(&m, 20, Some(42)).unwrap(); -// let b = run_shots_serial(&m, 20, Some(42)).unwrap(); -// assert_eq!(a, b); -// } - -// #[test] -// fn per_shot_seeds_differ() { -// // If every shot shared one seed, all 20 outcomes would be identical. -// // Distinct per-shot seeds must produce a mix of 0s and 1s. -// let m = module(RANDOM); -// let records = run_shots_serial(&m, 20, Some(42)).unwrap(); -// let first = &records[0]; -// assert!( -// records.iter().any(|r| r != first), -// "expected varied outcomes across shots, got {records:?}" -// ); -// } - -// #[test] -// fn nearby_seeds_do_not_share_shots() { -// // With `seed + i`, seed 8 was seed 7 shifted by one shot. -// let m = module(RANDOM); -// let a = run_shots_serial(&m, 128, Some(7)).unwrap(); -// let b = run_shots_serial(&m, 128, Some(8)).unwrap(); -// assert_ne!( -// a[1..], -// b[..127], -// "seed 8 reproduced seed 7's shots shifted by one" -// ); -// } - -// #[cfg(feature = "rayon")] -// #[test] -// fn serial_and_parallel_match_for_same_seed() { -// let m = module(RANDOM); -// let serial = run_shots_serial(&m, 64, Some(7)).unwrap(); -// let parallel = run_shots_parallel(&m, 64, Some(7)).unwrap(); -// assert_eq!(serial, parallel); -// } - -// #[cfg(feature = "rayon")] -// #[test] -// fn parallelizes_only_with_multiple_threads_above_threshold() { -// // A single-thread pool is always serial, no matter how many shots. -// assert!(!should_parallelize(1, 100_000)); -// // Multiple threads, but too few shots to amortize the overhead: serial. -// assert!(!should_parallelize(8, PARALLEL_SHOT_THRESHOLD - 1)); -// // Multiple threads at the threshold: parallel. -// assert!(should_parallelize(8, PARALLEL_SHOT_THRESHOLD)); -// // Multiple threads, plenty of shots: parallel. -// assert!(should_parallelize(8, 100_000)); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::compile_program; + use crate::measurements::MeasurementOutcome; + + /// Measures q0 in |0>: every shot is deterministically `0`. + const DETERMINISTIC: &[&str] = &[ + "execution::program::const_u64 0", + "circuit::circuit::measure", + "execution::program::ret 0", + ]; + + /// Prepares |+> with H, then measures q0: each shot is a random 0/1. + const RANDOM: &[&str] = &[ + "execution::program::const_u64 0", + "circuit::circuit::h", + "execution::program::const_u64 0", + "circuit::circuit::measure", + "execution::program::ret 0", + ]; + + /// Wrap `main`'s instructions in a 1-qubit SST program with its metadata. + fn module(main: &[&str]) -> PPVMModule { + let body = main.join("\n "); + let src = format!( + r#"sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata {{ + strings: ["main", "shots.sst"], + functions: [ + {{ + name: 0, + signature: {{ params: [], ret: [] }}, + local_count: 0, + start_address: 0, + end_address: {len}, + file: 1, + }}, + ], + labels: [], + constants: [], + main_function: 0, + source_symbols: [], + extra: {{}}, + }}; + .header(ppvm). + .text(ppvm): + fn @main() {{ + {body} + }} + .text(ppvm). + .section(ppvm). +.section(root). +"#, + len = main.len(), + ); + compile_program(&src).unwrap() + } + + #[test] + fn serial_runs_one_record_per_shot() { + let m = module(DETERMINISTIC); + let records = run_shots_serial(&m, 5, None).unwrap(); + assert_eq!(records.len(), 5); + for shot in &records { + // One measurement event, one qubit, deterministically |0>. + assert_eq!(shot.measurements.len(), 1); + assert_eq!(shot.measurements[0].as_slice(), [MeasurementOutcome::Zero]); + assert!(shot.traces.is_empty(), "Tableau backend emits no traces"); + } + } + + #[test] + fn dispatcher_runs_all_shots() { + let m = module(DETERMINISTIC); + // 10 shots is below the parallel threshold, so this takes the serial path. + let records = run_shots(&m, 10, None).unwrap(); + assert_eq!(records.len(), 10); + } + + #[test] + fn same_seed_is_reproducible() { + let m = module(RANDOM); + let a = run_shots_serial(&m, 20, Some(42)).unwrap(); + let b = run_shots_serial(&m, 20, Some(42)).unwrap(); + assert_eq!(a, b); + } + + #[test] + fn per_shot_seeds_differ() { + // If every shot shared one seed, all 20 outcomes would be identical. + // Distinct per-shot seeds must produce a mix of 0s and 1s. + let m = module(RANDOM); + let records = run_shots_serial(&m, 20, Some(42)).unwrap(); + let first = &records[0]; + assert!( + records.iter().any(|r| r != first), + "expected varied outcomes across shots, got {records:?}" + ); + } + + #[test] + fn nearby_seeds_do_not_share_shots() { + // With `seed + i`, seed 8 was seed 7 shifted by one shot. + let m = module(RANDOM); + let a = run_shots_serial(&m, 128, Some(7)).unwrap(); + let b = run_shots_serial(&m, 128, Some(8)).unwrap(); + assert_ne!( + a[1..], + b[..127], + "seed 8 reproduced seed 7's shots shifted by one" + ); + } + + #[cfg(feature = "rayon")] + #[test] + fn serial_and_parallel_match_for_same_seed() { + let m = module(RANDOM); + let serial = run_shots_serial(&m, 64, Some(7)).unwrap(); + let parallel = run_shots_parallel(&m, 64, Some(7)).unwrap(); + assert_eq!(serial, parallel); + } + + #[cfg(feature = "rayon")] + #[test] + fn parallelizes_only_with_multiple_threads_above_threshold() { + // A single-thread pool is always serial, no matter how many shots. + assert!(!should_parallelize(1, 100_000)); + // Multiple threads, but too few shots to amortize the overhead: serial. + assert!(!should_parallelize(8, PARALLEL_SHOT_THRESHOLD - 1)); + // Multiple threads at the threshold: parallel. + assert!(should_parallelize(8, PARALLEL_SHOT_THRESHOLD)); + // Multiple threads, plenty of shots: parallel. + assert!(should_parallelize(8, 100_000)); + } +} From 31c89586f9be2068b4931cb5e17c109db07bd40f Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 16:17:34 +0100 Subject: [PATCH 26/36] Comment out bytecode related stuff --- crates/ppvm-cli/src/commands.rs | 156 ++-- crates/ppvm-cli/src/main.rs | 45 +- crates/ppvm-vihaco/tests/sst_fixtures.rs | 866 ++++++++++++----------- 3 files changed, 541 insertions(+), 526 deletions(-) diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 9ffd2d1c7..fd17a2705 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -140,27 +140,28 @@ pub fn parse(file: &str, format: Format) -> Result<()> { Ok(()) } -pub fn dump(file: &str, output: Option<&str>, force: bool) -> Result<()> { - let output_file = match output { - Some(output_file_name) => output_file_name.to_string(), - None => Path::new(file) - .with_extension("ssb") - .to_string_lossy() - .into_owned(), - }; - - // Don't clobber an existing file unless asked to. - if !force && Path::new(&output_file).exists() { - return Err(eyre::eyre!( - "{output_file} already exists; pass --force to overwrite" - )); - } - - ppvm_vihaco::dump_file(file, &output_file) - .wrap_err_with(|| format!("failed to dump {file}"))?; - eprintln!("Bytecode written to {output_file}"); - Ok(()) -} +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. +// pub fn dump(file: &str, output: Option<&str>, force: bool) -> Result<()> { +// let output_file = match output { +// Some(output_file_name) => output_file_name.to_string(), +// None => Path::new(file) +// .with_extension("ssb") +// .to_string_lossy() +// .into_owned(), +// }; +// +// // Don't clobber an existing file unless asked to. +// if !force && Path::new(&output_file).exists() { +// return Err(eyre::eyre!( +// "{output_file} already exists; pass --force to overwrite" +// )); +// } +// +// ppvm_vihaco::dump_file(file, &output_file) +// .wrap_err_with(|| format!("failed to dump {file}"))?; +// eprintln!("Bytecode written to {output_file}"); +// Ok(()) +// } /// A command entered at the debugger prompt. enum DebugCommand { @@ -453,62 +454,63 @@ mod tests { assert!(err.to_string().contains("failed to read"), "got: {err}"); } - // ─── dump ────────────────────────────────────────────────────────── - - #[test] - fn dump_writes_default_ssb_path_when_output_omitted() { - let src = temp_file("ppvm_cli_dump_default.sst", PROGRAM); - let expected = Path::new(&src).with_extension("ssb"); - let _ = fs::remove_file(&expected); // clear any leftover from a prior run - - dump(&src, None, false).unwrap(); - assert!(expected.exists(), "default .ssb should have been written"); - - let _ = fs::remove_file(&src); - let _ = fs::remove_file(&expected); - } - - #[test] - fn dump_writes_explicit_output_path() { - let src = temp_file("ppvm_cli_dump_explicit.sst", PROGRAM); - let out = std::env::temp_dir().join("ppvm_cli_dump_explicit_out.ssb"); - let _ = fs::remove_file(&out); - - dump(&src, Some(out.to_str().unwrap()), false).unwrap(); - assert!(out.exists()); - - let _ = fs::remove_file(&src); - let _ = fs::remove_file(&out); - } - - #[test] - fn dump_refuses_to_clobber_without_force() { - let src = temp_file("ppvm_cli_dump_clobber.sst", PROGRAM); - let out = std::env::temp_dir().join("ppvm_cli_dump_clobber_out.ssb"); - fs::write(&out, b"existing").unwrap(); - - let err = dump(&src, Some(out.to_str().unwrap()), false).unwrap_err(); - assert!(err.to_string().contains("already exists"), "got: {err}"); - // The existing file must be left untouched. - assert_eq!(fs::read(&out).unwrap(), b"existing"); - - let _ = fs::remove_file(&src); - let _ = fs::remove_file(&out); - } - - #[test] - fn dump_overwrites_existing_with_force() { - let src = temp_file("ppvm_cli_dump_force.sst", PROGRAM); - let out = std::env::temp_dir().join("ppvm_cli_dump_force_out.ssb"); - fs::write(&out, b"existing").unwrap(); - - dump(&src, Some(out.to_str().unwrap()), true).unwrap(); - // Replaced with real bytecode, not the placeholder. - assert_ne!(fs::read(&out).unwrap(), b"existing"); - - let _ = fs::remove_file(&src); - let _ = fs::remove_file(&out); - } + // TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. + // // ─── dump ────────────────────────────────────────────────────────── + // + // #[test] + // fn dump_writes_default_ssb_path_when_output_omitted() { + // let src = temp_file("ppvm_cli_dump_default.sst", PROGRAM); + // let expected = Path::new(&src).with_extension("ssb"); + // let _ = fs::remove_file(&expected); // clear any leftover from a prior run + // + // dump(&src, None, false).unwrap(); + // assert!(expected.exists(), "default .ssb should have been written"); + // + // let _ = fs::remove_file(&src); + // let _ = fs::remove_file(&expected); + // } + // + // #[test] + // fn dump_writes_explicit_output_path() { + // let src = temp_file("ppvm_cli_dump_explicit.sst", PROGRAM); + // let out = std::env::temp_dir().join("ppvm_cli_dump_explicit_out.ssb"); + // let _ = fs::remove_file(&out); + // + // dump(&src, Some(out.to_str().unwrap()), false).unwrap(); + // assert!(out.exists()); + // + // let _ = fs::remove_file(&src); + // let _ = fs::remove_file(&out); + // } + // + // #[test] + // fn dump_refuses_to_clobber_without_force() { + // let src = temp_file("ppvm_cli_dump_clobber.sst", PROGRAM); + // let out = std::env::temp_dir().join("ppvm_cli_dump_clobber_out.ssb"); + // fs::write(&out, b"existing").unwrap(); + // + // let err = dump(&src, Some(out.to_str().unwrap()), false).unwrap_err(); + // assert!(err.to_string().contains("already exists"), "got: {err}"); + // // The existing file must be left untouched. + // assert_eq!(fs::read(&out).unwrap(), b"existing"); + // + // let _ = fs::remove_file(&src); + // let _ = fs::remove_file(&out); + // } + // + // #[test] + // fn dump_overwrites_existing_with_force() { + // let src = temp_file("ppvm_cli_dump_force.sst", PROGRAM); + // let out = std::env::temp_dir().join("ppvm_cli_dump_force_out.ssb"); + // fs::write(&out, b"existing").unwrap(); + // + // dump(&src, Some(out.to_str().unwrap()), true).unwrap(); + // // Replaced with real bytecode, not the placeholder. + // assert_ne!(fs::read(&out).unwrap(), b"existing"); + // + // let _ = fs::remove_file(&src); + // let _ = fs::remove_file(&out); + // } // ─── debug ───────────────────────────────────────────────────────── diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index a11098143..5a453b527 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -36,21 +36,21 @@ enum Commands { #[arg(short, long, value_enum, default_value = "pretty")] format: commands::Format, }, - /// Compile a .sst file to bytecode - Dump { - /// Input .sst file - #[arg(value_name = "FILE")] - file: String, - - /// Output file (optional, defaults to .ssb) - #[arg(short, long)] - output: Option, - - /// Overwrite the output file if it already exists - #[arg(short, long)] - force: bool, - }, - + // TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. + // /// Compile a .sst file to bytecode + // Dump { + // /// Input .sst file + // #[arg(value_name = "FILE")] + // file: String, + // + // /// Output file (optional, defaults to .ssb) + // #[arg(short, long)] + // output: Option, + // + // /// Overwrite the output file if it already exists + // #[arg(short, long)] + // force: bool, + // }, /// Run a .sst or .ssb program Run { /// Input file (.sst source or .ssb bytecode) @@ -103,13 +103,14 @@ fn main() -> Result<()> { Some(Commands::Parse { file, format }) => { commands::parse(&file, format)?; } - Some(Commands::Dump { - file, - output, - force, - }) => { - commands::dump(&file, output.as_deref(), force)?; - } + // TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. + // Some(Commands::Dump { + // file, + // output, + // force, + // }) => { + // commands::dump(&file, output.as_deref(), force)?; + // } Some(Commands::Run { file, shots, diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index ed8808914..755a2dbc7 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -1,77 +1,79 @@ -// // SPDX-FileCopyrightText: 2026 The PPVM Authors -// // SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 -// //! End-to-end fixture coverage: parse + resolve + run each `.sst` file in -// //! this directory via the public `PPVM` API. +//! End-to-end fixture coverage: parse + resolve + run each `.sst` file in +//! this directory via the public `PPVM` API. -// use ppvm_vihaco::composite::PPVM; -// use ppvm_vihaco::measurements::MeasurementOutcome; +use ppvm_vihaco::composite::PPVM; +use ppvm_vihaco::measurements::MeasurementOutcome; +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // /// Dump a fixture to a `.ssb` file, load it back, and run it. Exercises the // /// full bytecode round-trip through disk: `dump_file` → `load_bytecode_file`. // fn dump_load_run(sst_path: &str, ssb_name: &str) -> PPVM { // let out = std::env::temp_dir().join(ssb_name); // let out = out.to_str().expect("utf-8 temp path"); // ppvm_vihaco::dump_file(sst_path, out).unwrap_or_else(|e| panic!("dump {sst_path}: {e:?}")); - +// // let mut machine = PPVM::default(); // machine // .load_bytecode_file(out) // .unwrap_or_else(|e| panic!("load {out}: {e:?}")); // machine.run().unwrap_or_else(|e| panic!("run {out}: {e:?}")); - +// // let _ = std::fs::remove_file(out); // machine // } -// #[test] -// fn bell_sst_runs_and_records_two_measurements() { -// let mut machine = PPVM::default(); -// machine -// .run_file("tests/bell.sst") -// .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); -// assert_eq!(machine.measurement_record().len(), 2); -// } - -// #[test] -// fn hello_circuit_sst_parses_and_runs() { -// let mut machine = PPVM::default(); -// machine -// .run_file("tests/hello_circuit.sst") -// .unwrap_or_else(|e| panic!("run hello_circuit.sst: {e:?}")); -// // hello_circuit.sst applies H + CNOT + RX(0.1); no measurements. -// assert_eq!(machine.measurement_record().len(), 0); -// } - -// #[test] -// fn arithmetic_sst_uses_lhs_then_rhs_order() { -// let machine = ppvm_vihaco::run_file("tests/arithmetic.sst") -// .unwrap_or_else(|e| panic!("run arithmetic.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 1, "expected exactly one measurement"); -// assert_eq!( -// record[0].as_slice(), -// &[MeasurementOutcome::Zero], -// "all arithmetic checks should pass without flipping q0" -// ); -// } - -// #[test] -// fn rotxy_sst_runs_and_flips_qubit() { -// // `rotxy.sst` applies R(axis_angle=π/2, θ=π) = RY(π) to q0, deterministically -// // sending |0> → |1>, then measures it. Exercises the `circuit::circuit.r` path end to -// // end: parse → resolve (pop θ, axis_angle, qubit) → execute via `tab.r`. -// let machine = -// ppvm_vihaco::run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 1, "expected exactly one measurement"); -// assert_eq!( -// record[0].as_slice(), -// &[MeasurementOutcome::One], -// "R(π/2, π) = RY(π) must flip q0 to 1" -// ); -// } - +#[test] +fn bell_sst_runs_and_records_two_measurements() { + let mut machine = PPVM::default(); + machine + .run_file("tests/bell.sst") + .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); + assert_eq!(machine.measurement_record().len(), 2); +} + +#[test] +fn hello_circuit_sst_parses_and_runs() { + let mut machine = PPVM::default(); + machine + .run_file("tests/hello_circuit.sst") + .unwrap_or_else(|e| panic!("run hello_circuit.sst: {e:?}")); + // hello_circuit.sst applies H + CNOT + RX(0.1); no measurements. + assert_eq!(machine.measurement_record().len(), 0); +} + +#[test] +fn arithmetic_sst_uses_lhs_then_rhs_order() { + let machine = ppvm_vihaco::run_file("tests/arithmetic.sst") + .unwrap_or_else(|e| panic!("run arithmetic.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!( + record[0].as_slice(), + &[MeasurementOutcome::Zero], + "all arithmetic checks should pass without flipping q0" + ); +} + +#[test] +fn rotxy_sst_runs_and_flips_qubit() { + // `rotxy.sst` applies R(axis_angle=π/2, θ=π) = RY(π) to q0, deterministically + // sending |0> → |1>, then measures it. Exercises the `circuit::circuit.r` path end to + // end: parse → resolve (pop θ, axis_angle, qubit) → execute via `tab.r`. + let machine = + ppvm_vihaco::run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!( + record[0].as_slice(), + &[MeasurementOutcome::One], + "R(π/2, π) = RY(π) must flip q0 to 1" + ); +} + +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_rotxy_runs_and_flips_qubit() { // // Same program, but through the bytecode round-trip: confirms the `R` @@ -82,21 +84,22 @@ // assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); // } -// #[test] -// fn tableau_leakage_pins_to_one_and_skips_x() { -// // `tableau_leakage.sst` leaks q0 with (p0, p1) = (0, 1), pinning |1⟩, -// // then applies X. A leaked qubit is frozen, so measure still returns 1. -// let machine = ppvm_vihaco::run_file("tests/tableau_leakage.sst") -// .unwrap_or_else(|e| panic!("run tableau_leakage.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 1, "expected exactly one measurement"); -// assert_eq!( -// record[0].as_slice(), -// &[MeasurementOutcome::One], -// "leaked q0 must measure the pinned 1, not LOST, and X must be a no-op" -// ); -// } +#[test] +fn tableau_leakage_pins_to_one_and_skips_x() { + // `tableau_leakage.sst` leaks q0 with (p0, p1) = (0, 1), pinning |1⟩, + // then applies X. A leaked qubit is frozen, so measure still returns 1. + let machine = ppvm_vihaco::run_file("tests/tableau_leakage.sst") + .unwrap_or_else(|e| panic!("run tableau_leakage.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!( + record[0].as_slice(), + &[MeasurementOutcome::One], + "leaked q0 must measure the pinned 1, not LOST, and X must be a no-op" + ); +} +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_tableau_leakage_pins_to_one() { // let machine = dump_load_run("tests/tableau_leakage.sst", "ppvm_dump_tableau_leakage.ssb"); @@ -105,358 +108,359 @@ // assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); // } -// #[test] -// fn run_file_via_library_helper() { -// let machine = -// ppvm_vihaco::run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); -// assert_eq!(machine.measurement_record().len(), 2); -// } - -// #[test] -// fn function_call_jumps_into_callee_body() { -// // `function_call.sst` has main `call` into `@run_circuit`, which puts q1 -// // in |+>, measures it, and `halt`s. Verifies CallPatch resolves the -// // symbolic target and op_call actually transfers control there. -// let machine = ppvm_vihaco::run_file("tests/function_call.sst") -// .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 1, "expected exactly one measurement"); -// assert_eq!(record[0].len(), 1); -// assert!( -// record[0][0] != MeasurementOutcome::Lost, -// "measurement should not be lost" -// ); -// } - -// #[test] -// fn function_call_returns() { -// let machine = ppvm_vihaco::run_file("tests/function_call_ret.sst") -// .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 1, "expected exactly one measurement"); -// assert_eq!(record[0].len(), 1); -// assert!( -// record[0][0] != MeasurementOutcome::Lost, -// "measurement should not be lost" -// ); -// } - -// #[test] -// fn branch_on_outcome_deterministic_x_path() { -// // `branch_on_outcome_x.sst` applies X to q0 instead of H, so the outcome -// // is deterministically 1. The cpu::cpu.cond_br must therefore take the @one path, -// // which flips q1 before measuring it, yielding m1 = 1 as well. -// let machine = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") -// .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 2, "expected exactly two measurements"); -// assert_eq!( -// record[0].as_slice(), -// &[MeasurementOutcome::One], -// "X-prepared q0 must measure 1" -// ); -// assert_eq!( -// record[1].as_slice(), -// &[MeasurementOutcome::One], -// "branch must have flipped q1" -// ); -// } - -// #[test] -// fn branch_on_outcome_statistics_balanced_and_invariant_holds() { -// // `branch_on_outcome.sst` puts q0 in |+>, so its measurement is a fair -// // coin. The branch then flips q1 iff the outcome was 1, making m1 == m0 -// // an invariant on every shot. Run many shots and check both properties. -// const SHOTS: usize = 400; -// let mut ones = 0usize; -// for _ in 0..SHOTS { -// let machine = ppvm_vihaco::run_file("tests/branch_on_outcome.sst") -// .unwrap_or_else(|e| panic!("run branch_on_outcome.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 2); -// let m0 = record[0][0]; -// let m1 = record[1][0]; -// assert_eq!(m0, m1, "branch must steer q1 to match q0 on every shot"); -// assert!( -// m0 != MeasurementOutcome::Lost, -// "measurement should not be lost" -// ); -// if m0 == MeasurementOutcome::One { -// ones += 1; -// } -// } -// // Fair coin with SHOTS=400: mean=200, stddev=10. ±6σ window catches a -// // truly broken RNG without flaking on a healthy one. -// let lo = SHOTS / 2 - 60; -// let hi = SHOTS / 2 + 60; -// assert!( -// (lo..=hi).contains(&ones), -// "expected {lo}..={hi} ones out of {SHOTS}, got {ones}" -// ); -// } - -// #[test] -// fn function_call_branch_on_both_returned_values() { -// // `function_call_branch_both.sst`: helper returns the tri-state outcome -// // (0/1/Lost) via `ret 1`. Main first branches on is_lost, then on the -// // 0/1 outcome, steering q1 to |1> on the lost path and on the -// // kept-outcome=1 path, leaving q1 in |0> only on the kept-outcome=0 -// // path. With loss prob 0.5 and a |+> prep: -// // P(m1 = 1) = P(lost) + P(kept) · P(outcome = 1 | kept) -// // = 0.5 + 0.5 · 0.5 = 0.75 -// // P(m0 = lost) = 0.5 -// const SHOTS: usize = 400; -// let mut q0_lost = 0usize; -// let mut q1_ones = 0usize; -// for _ in 0..SHOTS { -// let machine = ppvm_vihaco::run_file("tests/function_call_branch_both.sst") -// .unwrap_or_else(|e| panic!("run function_call_branch_both.sst: {e:?}")); -// let record = machine.measurement_record(); -// assert_eq!(record.len(), 2, "expected exactly two measurements"); -// assert_eq!(record[0].len(), 1); -// assert_eq!(record[1].len(), 1); -// if record[0][0] == MeasurementOutcome::Lost { -// q0_lost += 1; -// } -// if record[1][0] == MeasurementOutcome::One { -// q1_ones += 1; -// } -// } -// // P(lost) = 0.5, SHOTS=400: mean=200, stddev=10. ±6σ window. -// assert!( -// (140..=260).contains(&q0_lost), -// "expected ~200 lost shots, got {q0_lost}" -// ); -// // P(m1=1) = 0.75, SHOTS=400: mean=300, stddev≈8.66. ±6σ → ~248..352. -// assert!( -// (240..=360).contains(&q1_ones), -// "expected ~300 q1=true shots, got {q1_ones}" -// ); -// } - -// // ─── Task 12: PauliSum / LossyPauliSum end-to-end via .sst source ──────── - -// #[test] -// fn paulisum_bell_zz_trace_through_sst() { -// // Bell-state ⟨ZZ⟩ via PauliSum. Textbook circuit H(0); CNOT(0,1) is -// // emitted reversed for Heisenberg propagation: `circuit::circuit.cnot; circuit::circuit.h`. -// // Conjugating ZZ by CNOT(0,1) gives Z_1 (= IZ); H on q0 leaves IZ -// // untouched. Tracing against |00> matches IZ (pattern `Z?*`) and -// // returns +1.0 — matching ⟨Φ+|ZZ|Φ+⟩ = 1. -// let machine = ppvm_vihaco::run_file("tests/paulisum_bell_trace.sst") -// .unwrap_or_else(|e| panic!("run paulisum_bell_trace.sst: {e:?}")); -// let trace = machine.trace_record(); -// assert_eq!(trace.len(), 1, "expected one trace emission"); -// assert!( -// (trace[0] - 1.0).abs() < 1e-12, -// "expected ⟨ZZ⟩ = 1.0, got {}", -// trace[0] -// ); -// } - -// #[test] -// fn paulisum_multi_term_observable_trace_through_sst() { -// // Phase 3 wiring: a 2-term observable `1.0*ZZ+0.5*XX` parses on the -// // header, seeds both terms, and an end-of-program trace picks up their -// // contributions. With no gates applied, tracing `[XZ]?*` matches both -// // ZZ and XX directly → coef sum = 1.0 + 0.5 = 1.5. -// let machine = ppvm_vihaco::run_file("tests/paulisum_multi_term_trace.sst") -// .unwrap_or_else(|e| panic!("run paulisum_multi_term_trace.sst: {e:?}")); -// let trace = machine.trace_record(); -// assert_eq!(trace.len(), 1); -// assert!( -// (trace[0] - 1.5).abs() < 1e-12, -// "expected 1.5, got {}", -// trace[0] -// ); -// } - -// #[test] -// fn paulisum_trotter_matches_pure_rust_reference() { -// // Two Trotter layers of RXX(0.1) + RZZ(0.05), interleaved with explicit -// // `circuit::circuit.truncate`. The .sst-driven path should agree bit-for-bit with a -// // pure Rust PauliSum running the same gates: `indexmap::ByteFxHashF64` -// // gives deterministic iteration order (Decision 7), so truncation order -// // and float accumulation are stable across both paths. - -// let machine = ppvm_vihaco::run_file("tests/paulisum_trotter_truncate.sst") -// .unwrap_or_else(|e| panic!("run paulisum_trotter_truncate.sst: {e:?}")); -// let through_sst = machine.trace_record(); -// assert_eq!(through_sst.len(), 1, "expected one trace emission"); - -// // Pure Rust reference: same N=8 / strategy / circuit order as the PauliSum -// // Bits64 bucket in `ppvm_vihaco::component`. -// use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; -// use ppvm_pauli_sum::prelude::*; -// use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; -// type RefConfig = ByteFxHashF64<8, CombinedStrategy>; - -// let mut state: PauliSum = PauliSum::builder() -// .n_qubits(2) -// .strategy(CombinedStrategy( -// CoefficientThreshold(1e-6), -// MaxPauliWeight(usize::MAX), -// )) -// .build(); -// state += ("ZZ", 1.0); -// state.rxx(0, 1, 0.1); -// state.rzz(0, 1, 0.05); -// state.truncate(); -// state.rxx(0, 1, 0.1); -// state.rzz(0, 1, 0.05); -// state.truncate(); -// let pat = PauliPattern::parse("Z?*").expect("parse pattern"); -// let reference = state.trace(&pat); - -// assert_eq!( -// through_sst[0], reference, -// ".sst-driven trace must match pure Rust reference bit-for-bit" -// ); -// } - -// #[test] -// fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { -// // End-to-end LossyPauliSum: header seeds ZZ, body applies CNOT(0,1) -// // then loss(q0, 0.3), then traces. The .sst-driven trace must agree -// // bit-for-bit with a pure-Rust reference using the same Config (a -// // `LossyPauliSumConfig<8>` matching the Bits64 bucket) and operations. -// let machine = ppvm_vihaco::run_file("tests/lossy_paulisum_loss_trace.sst") -// .unwrap_or_else(|e| panic!("run lossy_paulisum_loss_trace.sst: {e:?}")); -// let through_sst = machine.trace_record(); -// assert_eq!(through_sst.len(), 1, "expected one trace emission"); - -// use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; -// use ppvm_pauli_sum::prelude::*; -// use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; -// type RefConfig = ByteFxHashF64< -// 8, -// CombinedStrategy, -// LossyPauliWord<[u8; 8]>, -// >; - -// let mut state: PauliSum = PauliSum::builder() -// .n_qubits(2) -// .strategy(CombinedStrategy( -// CoefficientThreshold(1e-10), -// MaxPauliWeight(usize::MAX), -// )) -// .build(); -// state += ("ZZ", 1.0); -// state.cnot(0, 1); -// state.loss_channel(0, 0.3); -// state.truncate(); -// let pat = PauliPattern::parse("Z?*").expect("parse pattern"); -// let reference = state.trace(&pat); - -// assert_eq!( -// through_sst[0], reference, -// ".sst-driven trace must match pure Rust reference bit-for-bit" -// ); -// } - -// #[test] -// fn paulisum_measure_returns_unsupported_error() { -// // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a -// // clear "not supported on the PauliSum backend" error. -// let mut machine = PPVM::default(); -// machine -// .load_file("tests/paulisum_measure_error.sst") -// .unwrap_or_else(|e| panic!("load paulisum_measure_error.sst: {e:?}")); -// let err = machine.run().unwrap_err(); -// let msg = err.to_string(); -// assert!( -// msg.contains("not supported on the PauliSum backend"), -// "expected PauliSum-rejection error, got: {msg}" -// ); -// } - -// #[test] -// fn paulisum_leakage_returns_unsupported_error() { -// // Leakage is tableau-only. PauliSum must reject it the same way it -// // rejects Measure — not with a mismatched-argument fallback. -// let mut machine = PPVM::default(); -// machine -// .load_file("tests/paulisum_leakage_error.sst") -// .unwrap_or_else(|e| panic!("load paulisum_leakage_error.sst: {e:?}")); -// let err = machine.run().unwrap_err(); -// let msg = err.to_string(); -// assert!( -// msg.contains("not supported on the PauliSum backend"), -// "expected PauliSum-rejection error, got: {msg}" -// ); -// } - -// // ─── Task 16: Tableau-side Trace, cross-backend agreement ──────────────── - -// #[test] -// fn tableau_bell_zz_trace_through_sst() { -// // Bell-state ⟨ZZ⟩ via the Tableau backend's `Trace` instruction: -// // forward H(0); CNOT(0, 1) leaves |ψ⟩ = |Φ+⟩, and `Z0Z1` matches exactly -// // the Pauli word ZZ, so `tab.trace(&pat) = ⟨Φ+|ZZ|Φ+⟩ = 1`. -// let machine = ppvm_vihaco::run_file("tests/tableau_bell_trace.sst") -// .unwrap_or_else(|e| panic!("run tableau_bell_trace.sst: {e:?}")); -// let trace = machine.trace_record(); -// assert_eq!(trace.len(), 1, "expected one trace emission"); -// assert!( -// (trace[0] - 1.0).abs() < 1e-12, -// "expected ⟨ZZ⟩ = 1.0, got {}", -// trace[0] -// ); -// } - -// fn assert_cross_backend_agreement(tableau_sst: &str, paulisum_sst: &str) { -// let tab = -// ppvm_vihaco::run_file(tableau_sst).unwrap_or_else(|e| panic!("run {tableau_sst}: {e:?}")); -// let ps = -// ppvm_vihaco::run_file(paulisum_sst).unwrap_or_else(|e| panic!("run {paulisum_sst}: {e:?}")); -// let tab_v = tab.trace_record(); -// let ps_v = ps.trace_record(); -// assert_eq!(tab_v.len(), 1, "{tableau_sst}: expected one trace emission"); -// assert_eq!(ps_v.len(), 1, "{paulisum_sst}: expected one trace emission"); -// assert!( -// (tab_v[0] - ps_v[0]).abs() < 1e-12, -// "tableau {} vs PauliSum {} (|Δ|={}) for {tableau_sst} ↔ {paulisum_sst}", -// tab_v[0], -// ps_v[0], -// (tab_v[0] - ps_v[0]).abs() -// ); -// } - -// #[test] -// fn tableau_and_paulisum_agree_on_bell_zz_trace() { -// // ⟨Φ+|ZZ|Φ+⟩ = 1. Tableau forward-evolves |0…0⟩ and matches `Z0Z1`; -// // PauliSum Heisenberg-propagates ZZ backward through the reversed -// // circuit, then sums Z/I-only coefficients. -// assert_cross_backend_agreement( -// "tests/tableau_bell_trace.sst", -// "tests/paulisum_bell_trace.sst", -// ); -// } - -// #[test] -// fn tableau_and_paulisum_agree_on_ghz_xxx_trace() { -// // ⟨GHZ|XXX|GHZ⟩ = 1. Exercises a 3-qubit Clifford chain (H + two -// // CNOTs) and the non-trivial Heisenberg evolution XXX → ZII through -// // the reversed circuit on the PauliSum side. -// assert_cross_backend_agreement( -// "tests/tableau_ghz_xxx_trace.sst", -// "tests/paulisum_ghz_xxx_trace.sst", -// ); -// } - -// #[test] -// fn tableau_and_paulisum_agree_on_ry_z_trace() { -// // ⟨RY(θ)|0⟩|Z|RY(θ)|0⟩⟩ = cos(θ). Exercises a non-Clifford rotation: -// // the tableau opens a branch via `tab.ry`, while the PauliSum applies -// // the Heisenberg dual RY(θ)†·Z·RY(θ) = cos(θ)·Z + sin(θ)·X in one step. -// assert_cross_backend_agreement( -// "tests/tableau_ry_z_trace.sst", -// "tests/paulisum_ry_z_trace.sst", -// ); -// } - -// // ─── Auto-detect via load_file: route by content, not extension ─────────── - +#[test] +fn run_file_via_library_helper() { + let machine = + ppvm_vihaco::run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); + assert_eq!(machine.measurement_record().len(), 2); +} + +#[test] +fn function_call_jumps_into_callee_body() { + // `function_call.sst` has main `call` into `@run_circuit`, which puts q1 + // in |+>, measures it, and `halt`s. Verifies CallPatch resolves the + // symbolic target and op_call actually transfers control there. + let machine = ppvm_vihaco::run_file("tests/function_call.sst") + .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!(record[0].len(), 1); + assert!( + record[0][0] != MeasurementOutcome::Lost, + "measurement should not be lost" + ); +} + +#[test] +fn function_call_returns() { + let machine = ppvm_vihaco::run_file("tests/function_call_ret.sst") + .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!(record[0].len(), 1); + assert!( + record[0][0] != MeasurementOutcome::Lost, + "measurement should not be lost" + ); +} + +#[test] +fn branch_on_outcome_deterministic_x_path() { + // `branch_on_outcome_x.sst` applies X to q0 instead of H, so the outcome + // is deterministically 1. The cpu::cpu.cond_br must therefore take the @one path, + // which flips q1 before measuring it, yielding m1 = 1 as well. + let machine = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") + .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2, "expected exactly two measurements"); + assert_eq!( + record[0].as_slice(), + &[MeasurementOutcome::One], + "X-prepared q0 must measure 1" + ); + assert_eq!( + record[1].as_slice(), + &[MeasurementOutcome::One], + "branch must have flipped q1" + ); +} + +#[test] +fn branch_on_outcome_statistics_balanced_and_invariant_holds() { + // `branch_on_outcome.sst` puts q0 in |+>, so its measurement is a fair + // coin. The branch then flips q1 iff the outcome was 1, making m1 == m0 + // an invariant on every shot. Run many shots and check both properties. + const SHOTS: usize = 400; + let mut ones = 0usize; + for _ in 0..SHOTS { + let machine = ppvm_vihaco::run_file("tests/branch_on_outcome.sst") + .unwrap_or_else(|e| panic!("run branch_on_outcome.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2); + let m0 = record[0][0]; + let m1 = record[1][0]; + assert_eq!(m0, m1, "branch must steer q1 to match q0 on every shot"); + assert!( + m0 != MeasurementOutcome::Lost, + "measurement should not be lost" + ); + if m0 == MeasurementOutcome::One { + ones += 1; + } + } + // Fair coin with SHOTS=400: mean=200, stddev=10. ±6σ window catches a + // truly broken RNG without flaking on a healthy one. + let lo = SHOTS / 2 - 60; + let hi = SHOTS / 2 + 60; + assert!( + (lo..=hi).contains(&ones), + "expected {lo}..={hi} ones out of {SHOTS}, got {ones}" + ); +} + +#[test] +fn function_call_branch_on_both_returned_values() { + // `function_call_branch_both.sst`: helper returns the tri-state outcome + // (0/1/Lost) via `ret 1`. Main first branches on is_lost, then on the + // 0/1 outcome, steering q1 to |1> on the lost path and on the + // kept-outcome=1 path, leaving q1 in |0> only on the kept-outcome=0 + // path. With loss prob 0.5 and a |+> prep: + // P(m1 = 1) = P(lost) + P(kept) · P(outcome = 1 | kept) + // = 0.5 + 0.5 · 0.5 = 0.75 + // P(m0 = lost) = 0.5 + const SHOTS: usize = 400; + let mut q0_lost = 0usize; + let mut q1_ones = 0usize; + for _ in 0..SHOTS { + let machine = ppvm_vihaco::run_file("tests/function_call_branch_both.sst") + .unwrap_or_else(|e| panic!("run function_call_branch_both.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2, "expected exactly two measurements"); + assert_eq!(record[0].len(), 1); + assert_eq!(record[1].len(), 1); + if record[0][0] == MeasurementOutcome::Lost { + q0_lost += 1; + } + if record[1][0] == MeasurementOutcome::One { + q1_ones += 1; + } + } + // P(lost) = 0.5, SHOTS=400: mean=200, stddev=10. ±6σ window. + assert!( + (140..=260).contains(&q0_lost), + "expected ~200 lost shots, got {q0_lost}" + ); + // P(m1=1) = 0.75, SHOTS=400: mean=300, stddev≈8.66. ±6σ → ~248..352. + assert!( + (240..=360).contains(&q1_ones), + "expected ~300 q1=true shots, got {q1_ones}" + ); +} + +// ─── Task 12: PauliSum / LossyPauliSum end-to-end via .sst source ──────── + +#[test] +fn paulisum_bell_zz_trace_through_sst() { + // Bell-state ⟨ZZ⟩ via PauliSum. Textbook circuit H(0); CNOT(0,1) is + // emitted reversed for Heisenberg propagation: `circuit::circuit.cnot; circuit::circuit.h`. + // Conjugating ZZ by CNOT(0,1) gives Z_1 (= IZ); H on q0 leaves IZ + // untouched. Tracing against |00> matches IZ (pattern `Z?*`) and + // returns +1.0 — matching ⟨Φ+|ZZ|Φ+⟩ = 1. + let machine = ppvm_vihaco::run_file("tests/paulisum_bell_trace.sst") + .unwrap_or_else(|e| panic!("run paulisum_bell_trace.sst: {e:?}")); + let trace = machine.trace_record(); + assert_eq!(trace.len(), 1, "expected one trace emission"); + assert!( + (trace[0] - 1.0).abs() < 1e-12, + "expected ⟨ZZ⟩ = 1.0, got {}", + trace[0] + ); +} + +#[test] +fn paulisum_multi_term_observable_trace_through_sst() { + // Phase 3 wiring: a 2-term observable `1.0*ZZ+0.5*XX` parses on the + // header, seeds both terms, and an end-of-program trace picks up their + // contributions. With no gates applied, tracing `[XZ]?*` matches both + // ZZ and XX directly → coef sum = 1.0 + 0.5 = 1.5. + let machine = ppvm_vihaco::run_file("tests/paulisum_multi_term_trace.sst") + .unwrap_or_else(|e| panic!("run paulisum_multi_term_trace.sst: {e:?}")); + let trace = machine.trace_record(); + assert_eq!(trace.len(), 1); + assert!( + (trace[0] - 1.5).abs() < 1e-12, + "expected 1.5, got {}", + trace[0] + ); +} + +#[test] +fn paulisum_trotter_matches_pure_rust_reference() { + // Two Trotter layers of RXX(0.1) + RZZ(0.05), interleaved with explicit + // `circuit::circuit.truncate`. The .sst-driven path should agree bit-for-bit with a + // pure Rust PauliSum running the same gates: `indexmap::ByteFxHashF64` + // gives deterministic iteration order (Decision 7), so truncation order + // and float accumulation are stable across both paths. + + let machine = ppvm_vihaco::run_file("tests/paulisum_trotter_truncate.sst") + .unwrap_or_else(|e| panic!("run paulisum_trotter_truncate.sst: {e:?}")); + let through_sst = machine.trace_record(); + assert_eq!(through_sst.len(), 1, "expected one trace emission"); + + // Pure Rust reference: same N=8 / strategy / circuit order as the PauliSum + // Bits64 bucket in `ppvm_vihaco::component`. + use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; + use ppvm_pauli_sum::prelude::*; + use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; + type RefConfig = ByteFxHashF64<8, CombinedStrategy>; + + let mut state: PauliSum = PauliSum::builder() + .n_qubits(2) + .strategy(CombinedStrategy( + CoefficientThreshold(1e-6), + MaxPauliWeight(usize::MAX), + )) + .build(); + state += ("ZZ", 1.0); + state.rxx(0, 1, 0.1); + state.rzz(0, 1, 0.05); + state.truncate(); + state.rxx(0, 1, 0.1); + state.rzz(0, 1, 0.05); + state.truncate(); + let pat = PauliPattern::parse("Z?*").expect("parse pattern"); + let reference = state.trace(&pat); + + assert_eq!( + through_sst[0], reference, + ".sst-driven trace must match pure Rust reference bit-for-bit" + ); +} + +#[test] +fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { + // End-to-end LossyPauliSum: header seeds ZZ, body applies CNOT(0,1) + // then loss(q0, 0.3), then traces. The .sst-driven trace must agree + // bit-for-bit with a pure-Rust reference using the same Config (a + // `LossyPauliSumConfig<8>` matching the Bits64 bucket) and operations. + let machine = ppvm_vihaco::run_file("tests/lossy_paulisum_loss_trace.sst") + .unwrap_or_else(|e| panic!("run lossy_paulisum_loss_trace.sst: {e:?}")); + let through_sst = machine.trace_record(); + assert_eq!(through_sst.len(), 1, "expected one trace emission"); + + use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; + use ppvm_pauli_sum::prelude::*; + use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; + type RefConfig = ByteFxHashF64< + 8, + CombinedStrategy, + LossyPauliWord<[u8; 8]>, + >; + + let mut state: PauliSum = PauliSum::builder() + .n_qubits(2) + .strategy(CombinedStrategy( + CoefficientThreshold(1e-10), + MaxPauliWeight(usize::MAX), + )) + .build(); + state += ("ZZ", 1.0); + state.cnot(0, 1); + state.loss_channel(0, 0.3); + state.truncate(); + let pat = PauliPattern::parse("Z?*").expect("parse pattern"); + let reference = state.trace(&pat); + + assert_eq!( + through_sst[0], reference, + ".sst-driven trace must match pure Rust reference bit-for-bit" + ); +} + +#[test] +fn paulisum_measure_returns_unsupported_error() { + // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a + // clear "not supported on the PauliSum backend" error. + let mut machine = PPVM::default(); + machine + .load_file("tests/paulisum_measure_error.sst") + .unwrap_or_else(|e| panic!("load paulisum_measure_error.sst: {e:?}")); + let err = machine.run().unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("not supported on the PauliSum backend"), + "expected PauliSum-rejection error, got: {msg}" + ); +} + +#[test] +fn paulisum_leakage_returns_unsupported_error() { + // Leakage is tableau-only. PauliSum must reject it the same way it + // rejects Measure — not with a mismatched-argument fallback. + let mut machine = PPVM::default(); + machine + .load_file("tests/paulisum_leakage_error.sst") + .unwrap_or_else(|e| panic!("load paulisum_leakage_error.sst: {e:?}")); + let err = machine.run().unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("not supported on the PauliSum backend"), + "expected PauliSum-rejection error, got: {msg}" + ); +} + +// ─── Task 16: Tableau-side Trace, cross-backend agreement ──────────────── + +#[test] +fn tableau_bell_zz_trace_through_sst() { + // Bell-state ⟨ZZ⟩ via the Tableau backend's `Trace` instruction: + // forward H(0); CNOT(0, 1) leaves |ψ⟩ = |Φ+⟩, and `Z0Z1` matches exactly + // the Pauli word ZZ, so `tab.trace(&pat) = ⟨Φ+|ZZ|Φ+⟩ = 1`. + let machine = ppvm_vihaco::run_file("tests/tableau_bell_trace.sst") + .unwrap_or_else(|e| panic!("run tableau_bell_trace.sst: {e:?}")); + let trace = machine.trace_record(); + assert_eq!(trace.len(), 1, "expected one trace emission"); + assert!( + (trace[0] - 1.0).abs() < 1e-12, + "expected ⟨ZZ⟩ = 1.0, got {}", + trace[0] + ); +} + +fn assert_cross_backend_agreement(tableau_sst: &str, paulisum_sst: &str) { + let tab = + ppvm_vihaco::run_file(tableau_sst).unwrap_or_else(|e| panic!("run {tableau_sst}: {e:?}")); + let ps = + ppvm_vihaco::run_file(paulisum_sst).unwrap_or_else(|e| panic!("run {paulisum_sst}: {e:?}")); + let tab_v = tab.trace_record(); + let ps_v = ps.trace_record(); + assert_eq!(tab_v.len(), 1, "{tableau_sst}: expected one trace emission"); + assert_eq!(ps_v.len(), 1, "{paulisum_sst}: expected one trace emission"); + assert!( + (tab_v[0] - ps_v[0]).abs() < 1e-12, + "tableau {} vs PauliSum {} (|Δ|={}) for {tableau_sst} ↔ {paulisum_sst}", + tab_v[0], + ps_v[0], + (tab_v[0] - ps_v[0]).abs() + ); +} + +#[test] +fn tableau_and_paulisum_agree_on_bell_zz_trace() { + // ⟨Φ+|ZZ|Φ+⟩ = 1. Tableau forward-evolves |0…0⟩ and matches `Z0Z1`; + // PauliSum Heisenberg-propagates ZZ backward through the reversed + // circuit, then sums Z/I-only coefficients. + assert_cross_backend_agreement( + "tests/tableau_bell_trace.sst", + "tests/paulisum_bell_trace.sst", + ); +} + +#[test] +fn tableau_and_paulisum_agree_on_ghz_xxx_trace() { + // ⟨GHZ|XXX|GHZ⟩ = 1. Exercises a 3-qubit Clifford chain (H + two + // CNOTs) and the non-trivial Heisenberg evolution XXX → ZII through + // the reversed circuit on the PauliSum side. + assert_cross_backend_agreement( + "tests/tableau_ghz_xxx_trace.sst", + "tests/paulisum_ghz_xxx_trace.sst", + ); +} + +#[test] +fn tableau_and_paulisum_agree_on_ry_z_trace() { + // ⟨RY(θ)|0⟩|Z|RY(θ)|0⟩⟩ = cos(θ). Exercises a non-Clifford rotation: + // the tableau opens a branch via `tab.ry`, while the PauliSum applies + // the Heisenberg dual RY(θ)†·Z·RY(θ) = cos(θ)·Z + sin(θ)·X in one step. + assert_cross_backend_agreement( + "tests/tableau_ry_z_trace.sst", + "tests/paulisum_ry_z_trace.sst", + ); +} + +// ─── Auto-detect via load_file: route by content, not extension ─────────── + +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn is_bytecode_distinguishes_ssb_from_sst() { // let sst = std::fs::read("tests/bell.sst").expect("read bell.sst"); @@ -464,7 +468,7 @@ // !ppvm_vihaco::bytecode::is_bytecode(&sst), // ".sst source must not be detected as bytecode" // ); - +// // let ssb = ppvm_vihaco::bytecode::compile_to_bytes( // &String::from_utf8(sst).expect("bell.sst is utf-8"), // ) @@ -473,7 +477,7 @@ // ppvm_vihaco::bytecode::is_bytecode(&ssb), // "dumped .ssb must be detected as bytecode" // ); - +// // // Inputs shorter than the 4-byte magic are never bytecode. Note "PPVM" as // // text also fails: the magic is a little-endian u32, so its on-disk bytes // // are "MVPP", not "PPVM". @@ -482,6 +486,7 @@ // assert!(!ppvm_vihaco::bytecode::is_bytecode(b"PPVM")); // } +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn load_file_auto_detects_bytecode_and_text() { // // Use the deterministic X-prepared fixture: q0 measures 1, the branch @@ -489,7 +494,7 @@ // // or a binary file mis-parsed as text — fails loudly. // let from_text = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") // .unwrap_or_else(|e| panic!("run .sst via load_file: {e:?}")); - +// // // Dump the same fixture to a `.ssb` and run *that file* through the same // // run_file entry point. If load_file didn't sniff the magic it would try // // to parse the binary as text and error. @@ -499,7 +504,7 @@ // .unwrap_or_else(|e| panic!("dump: {e:?}")); // let from_binary = ppvm_vihaco::run_file(out).unwrap_or_else(|e| panic!("run .ssb: {e:?}")); // let _ = std::fs::remove_file(out); - +// // for (label, machine) in [("text", &from_text), ("binary", &from_binary)] { // let record = machine.measurement_record(); // assert_eq!(record.len(), 2, "{label}: expected two measurements"); @@ -516,20 +521,23 @@ // } // } -// // ─── Bytecode round-trip: dump → load → execute each fixture ────────────── +// ─── Bytecode round-trip: dump → load → execute each fixture ────────────── +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_bell_records_two_measurements() { // let machine = dump_load_run("tests/bell.sst", "ppvm_dump_bell.ssb"); // assert_eq!(machine.measurement_record().len(), 2); // } +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_hello_circuit_runs_with_no_measurements() { // let machine = dump_load_run("tests/hello_circuit.sst", "ppvm_dump_hello_circuit.ssb"); // assert_eq!(machine.measurement_record().len(), 0); // } +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_function_call_executes_callee() { // let machine = dump_load_run("tests/function_call.sst", "ppvm_dump_function_call.ssb"); @@ -539,6 +547,7 @@ // assert!(record[0][0] != MeasurementOutcome::Lost); // } +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_function_call_ret_executes() { // let machine = dump_load_run( @@ -550,6 +559,7 @@ // assert_eq!(record[0].len(), 1); // } +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_branch_on_outcome_x_is_deterministic() { // // X-prepared q0 measures 1, so the branch flips q1 → both outcomes are 1. @@ -561,6 +571,7 @@ // assert_eq!(record[1].as_slice(), &[MeasurementOutcome::One]); // } +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_branch_on_outcome_preserves_invariant() { // // q0 in |+> is a fair coin, but the branch steers q1 to match q0 every @@ -571,6 +582,7 @@ // assert_eq!(record[0][0], record[1][0]); // } +// TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // #[test] // fn dumped_function_call_branch_both_runs() { // let machine = dump_load_run( From beab9827d134dd52bd61fa2b8b5769754e0eeae6 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 16:22:07 +0100 Subject: [PATCH 27/36] Port over sst fixtures --- crates/ppvm-vihaco/tests/arithmetic.sst | 154 ++++++++++------ crates/ppvm-vihaco/tests/bell.sst | 54 ++++-- .../ppvm-vihaco/tests/branch_on_outcome.sst | 76 +++++--- .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 78 +++++--- crates/ppvm-vihaco/tests/function_call.sst | 66 +++++-- .../tests/function_call_branch_both.sst | 167 +++++++++++------- .../ppvm-vihaco/tests/function_call_ret.sst | 99 +++++++---- crates/ppvm-vihaco/tests/hello_circuit.sst | 52 ++++-- .../tests/lossy_paulisum_loss_trace.sst | 76 +++++--- .../ppvm-vihaco/tests/paulisum_bell_trace.sst | 58 ++++-- .../tests/paulisum_ghz_xxx_trace.sst | 74 +++++--- .../tests/paulisum_leakage_error.sst | 52 ++++-- .../tests/paulisum_measure_error.sst | 44 ++++- .../tests/paulisum_multi_term_trace.sst | 48 +++-- .../ppvm-vihaco/tests/paulisum_ry_z_trace.sst | 58 ++++-- .../tests/paulisum_trotter_truncate.sst | 88 +++++---- crates/ppvm-vihaco/tests/rotxy.sst | 52 ++++-- crates/ppvm-vihaco/tests/sst_fixtures.rs | 82 ++++----- .../ppvm-vihaco/tests/tableau_bell_trace.sst | 58 ++++-- .../tests/tableau_ghz_xxx_trace.sst | 60 +++++-- crates/ppvm-vihaco/tests/tableau_leakage.sst | 60 +++++-- .../ppvm-vihaco/tests/tableau_ry_z_trace.sst | 50 ++++-- 22 files changed, 1103 insertions(+), 503 deletions(-) diff --git a/crates/ppvm-vihaco/tests/arithmetic.sst b/crates/ppvm-vihaco/tests/arithmetic.sst index df0d35b32..8c9aab50c 100644 --- a/crates/ppvm-vihaco/tests/arithmetic.sst +++ b/crates/ppvm-vihaco/tests/arithmetic.sst @@ -1,65 +1,103 @@ -device circuit.n_qubits 1; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "arithmetic.sst", "check_div", "check_rem", "check_mul", "check_add", "fail_sub", "fail_div", "fail_rem", "fail_mul", "fail_add", "fail", "success"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 41, + file: 1, + }, + ], + labels: [ + { name: 2, address: 6 }, // check_div + { name: 3, address: 12 }, // check_rem + { name: 4, address: 18 }, // check_mul + { name: 5, address: 24 }, // check_add + { name: 6, address: 30 }, // fail_sub + { name: 7, address: 31 }, // fail_div + { name: 8, address: 32 }, // fail_rem + { name: 9, address: 33 }, // fail_mul + { name: 10, address: 34 }, // fail_add + { name: 11, address: 35 }, // fail + { name: 12, address: 38 }, // success + ], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + // Binary arithmetic pushes operands in source order: lhs, then rhs. + // The CPU pops rhs first and lhs second before applying the operation. + fn @main() { + execution::program::const_u64 10 + execution::program::const_u64 3 + cpu::cpu::sub_u64 + execution::program::const_u64 7 + cpu::cpu::eq_u64 + execution::program::cond_br @check_div, @fail_sub -// Binary arithmetic pushes operands in source order: lhs, then rhs. -// The CPU pops rhs first and lhs second before applying the operation. -fn @main() { - cpu::cpu.const u64, 10 - cpu::cpu.const u64, 3 - cpu::cpu.sub u64 - cpu::cpu.const u64, 7 - cpu::cpu.eq u64 - cpu::cpu.cond_br @check_div, @fail_sub + label @check_div + execution::program::const_u64 10 + execution::program::const_u64 3 + cpu::cpu::div_u64 + execution::program::const_u64 3 + cpu::cpu::eq_u64 + execution::program::cond_br @check_rem, @fail_div -cpu::cpu.label @check_div - cpu::cpu.const u64, 10 - cpu::cpu.const u64, 3 - cpu::cpu.div u64 - cpu::cpu.const u64, 3 - cpu::cpu.eq u64 - cpu::cpu.cond_br @check_rem, @fail_div + label @check_rem + execution::program::const_u64 10 + execution::program::const_u64 3 + cpu::cpu::rem_u64 + execution::program::const_u64 1 + cpu::cpu::eq_u64 + execution::program::cond_br @check_mul, @fail_rem -cpu::cpu.label @check_rem - cpu::cpu.const u64, 10 - cpu::cpu.const u64, 3 - cpu::cpu.rem u64 - cpu::cpu.const u64, 1 - cpu::cpu.eq u64 - cpu::cpu.cond_br @check_mul, @fail_rem + label @check_mul + execution::program::const_u64 10 + execution::program::const_u64 3 + cpu::cpu::mul_u64 + execution::program::const_u64 30 + cpu::cpu::eq_u64 + execution::program::cond_br @check_add, @fail_mul -cpu::cpu.label @check_mul - cpu::cpu.const u64, 10 - cpu::cpu.const u64, 3 - cpu::cpu.mul u64 - cpu::cpu.const u64, 30 - cpu::cpu.eq u64 - cpu::cpu.cond_br @check_add, @fail_mul + label @check_add + execution::program::const_u64 10 + execution::program::const_u64 3 + cpu::cpu::add_u64 + execution::program::const_u64 13 + cpu::cpu::eq_u64 + execution::program::cond_br @success, @fail_add -cpu::cpu.label @check_add - cpu::cpu.const u64, 10 - cpu::cpu.const u64, 3 - cpu::cpu.add u64 - cpu::cpu.const u64, 13 - cpu::cpu.eq u64 - cpu::cpu.cond_br @success, @fail_add + label @fail_sub + execution::program::br @fail + label @fail_div + execution::program::br @fail + label @fail_rem + execution::program::br @fail + label @fail_mul + execution::program::br @fail + label @fail_add + execution::program::br @fail -cpu::cpu.label @fail_sub - cpu::cpu.br @fail -cpu::cpu.label @fail_div - cpu::cpu.br @fail -cpu::cpu.label @fail_rem - cpu::cpu.br @fail -cpu::cpu.label @fail_mul - cpu::cpu.br @fail -cpu::cpu.label @fail_add - cpu::cpu.br @fail + label @fail + execution::program::const_u64 0 + circuit::circuit::x + execution::program::br @success -cpu::cpu.label @fail - cpu::cpu.const u64, 0 - circuit::circuit.x - cpu::cpu.br @success - -cpu::cpu.label @success - cpu::cpu.const u64, 0 - circuit::circuit.measure - cpu::cpu.ret 0 -} + label @success + execution::program::const_u64 0 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index 7b7e3119f..7db12526b 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -1,18 +1,44 @@ -device circuit.n_qubits 2; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "bell.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 10, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + execution::program::const_u64 0 + circuit::circuit::h -fn @main() { - cpu::cpu.const u64, 0 - circuit::circuit.h + execution::program::const_u64 0 + execution::program::const_u64 1 + circuit::circuit::cnot - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot + execution::program::const_u64 0 + circuit::circuit::measure - cpu::cpu.const u64, 0 - circuit::circuit.measure + execution::program::const_u64 1 + circuit::circuit::measure - cpu::cpu.const u64, 1 - circuit::circuit.measure - - cpu::cpu.ret 0 -} + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index 43b8190e5..1af5467b2 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -1,29 +1,59 @@ -device circuit.n_qubits 2; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "branch_on_outcome.sst", "one", "zero", "measure_q1"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 14, + file: 1, + }, + ], + labels: [ + { name: 2, address: 7 }, // one + { name: 3, address: 10 }, // zero + { name: 4, address: 11 }, // measure_q1 + ], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + execution::program::const_u64 0 + circuit::circuit::h -fn @main() { - cpu::cpu.const u64, 0 - circuit::circuit.h + execution::program::const_u64 0 + circuit::circuit::measure - cpu::cpu.const u64, 0 - circuit::circuit.measure + // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 + // to derive a bool for cond_br. + execution::program::const_u32 1 + cpu::cpu::eq_u32 - // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 - // to derive a bool for cond_br. - cpu::cpu.const u32, 1 - cpu::cpu.eq u32 + execution::program::cond_br @one, @zero - cpu::cpu.cond_br @one, @zero + label @one + execution::program::const_u64 1 + circuit::circuit::x + execution::program::br @measure_q1 - cpu::cpu.label @one - cpu::cpu.const u64, 1 - circuit::circuit.x - cpu::cpu.br @measure_q1 + label @zero + execution::program::br @measure_q1 - cpu::cpu.label @zero - cpu::cpu.br @measure_q1 - - cpu::cpu.label @measure_q1 - cpu::cpu.const u64, 1 - circuit::circuit.measure - cpu::cpu.ret 0 -} + label @measure_q1 + execution::program::const_u64 1 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index f877999a0..a2985b736 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -1,30 +1,60 @@ -device circuit.n_qubits 2; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "branch_on_outcome_x.sst", "one", "zero", "measure_q1"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 14, + file: 1, + }, + ], + labels: [ + { name: 2, address: 7 }, // one + { name: 3, address: 10 }, // zero + { name: 4, address: 11 }, // measure_q1 + ], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // X on q0 -> |1>, measure -> outcome is deterministically 1. + execution::program::const_u64 0 + circuit::circuit::x -fn @main() { - // X on q0 -> |1>, measure -> outcome is deterministically 1. - cpu::cpu.const u64, 0 - circuit::circuit.x + execution::program::const_u64 0 + circuit::circuit::measure - cpu::cpu.const u64, 0 - circuit::circuit.measure + // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 + // to derive a bool for cond_br. + execution::program::const_u32 1 + cpu::cpu::eq_u32 - // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 - // to derive a bool for cond_br. - cpu::cpu.const u32, 1 - cpu::cpu.eq u32 + execution::program::cond_br @one, @zero - cpu::cpu.cond_br @one, @zero + label @one + execution::program::const_u64 1 + circuit::circuit::x + execution::program::br @measure_q1 - cpu::cpu.label @one - cpu::cpu.const u64, 1 - circuit::circuit.x - cpu::cpu.br @measure_q1 + label @zero + execution::program::br @measure_q1 - cpu::cpu.label @zero - cpu::cpu.br @measure_q1 - - cpu::cpu.label @measure_q1 - cpu::cpu.const u64, 1 - circuit::circuit.measure - cpu::cpu.ret 0 -} + label @measure_q1 + execution::program::const_u64 1 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index 1b549f4de..74d2f2d00 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -1,19 +1,53 @@ -device circuit.n_qubits 2; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "run_circuit", "function_call.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 2, + file: 2, + }, + { + name: 1, // run_circuit + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 2, + end_address: 7, + file: 2, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Jump into the helper, which finishes the program with `halt`. + // Using `halt` instead of `ret` from the callee avoids depending on + // vihaco-cpu restoring a return PC, which it doesn't track today. + execution::program::call 0, @run_circuit + execution::program::ret 0 + } -fn @main() { - // Jump into the helper, which finishes the program with `halt`. - // Using `halt` instead of `ret` from the callee avoids depending on - // vihaco-cpu restoring a return PC, which it doesn't track today. - cpu::cpu.call 0, run_circuit - cpu::cpu.ret 0 -} + fn @run_circuit() { + execution::program::const_u64 1 + circuit::circuit::h -fn @run_circuit() { - cpu::cpu.const u64, 1 - circuit::circuit.h + execution::program::const_u64 1 + circuit::circuit::measure - cpu::cpu.const u64, 1 - circuit::circuit.measure - - cpu::cpu.ret 1 -} + execution::program::ret 1 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/function_call_branch_both.sst b/crates/ppvm-vihaco/tests/function_call_branch_both.sst index f16e00afe..e71060bf3 100644 --- a/crates/ppvm-vihaco/tests/function_call_branch_both.sst +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -1,63 +1,104 @@ -device circuit.n_qubits 2; - -// Exercises a helper that returns the tri-state measurement outcome via -// `ret 1`, with main branching on loss first, then on the 0/1 outcome. -// -// Layout after `call 0, @measure_q0` returns: stack = [outcome] where -// outcome ∈ {0, 1, 2} (2 = Lost). -// -// Loss prob is 0.5, so: -// P(lost) = 0.5 → q1 flipped to |1> → m1 = 1 -// P(kept ∧ outcome = 1) = 0.25 → q1 flipped to |1> → m1 = 1 -// P(kept ∧ outcome = 0) = 0.25 → q1 stays in |0> → m1 = 0 -// → P(m1 = 1) = 0.75. -fn @main() { - cpu::cpu.const u64, 0 - circuit::circuit.h - - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 0.5 - circuit::circuit.loss - - cpu::cpu.call 0, measure_q0 - - // Stack: [outcome]. Branch on Lost (outcome == 2) first. - cpu::cpu.dup - cpu::cpu.const u32, 2 - cpu::cpu.eq u32 - cpu::cpu.cond_br @lost, @kept - - cpu::cpu.label @lost - // is_lost = true: outcome is meaningless. Flip q1 to mark "lost" - // path. The leftover `outcome` value on the stack is harmless because - // we halt at @final without reading it. - cpu::cpu.const u64, 1 - circuit::circuit.x - cpu::cpu.br @final - - cpu::cpu.label @kept - // Stack: [outcome (0 or 1)]. Compare to 1 to derive a bool. - cpu::cpu.const u32, 1 - cpu::cpu.eq u32 - cpu::cpu.cond_br @outcome_one, @outcome_zero - - cpu::cpu.label @outcome_one - cpu::cpu.const u64, 1 - circuit::circuit.x - cpu::cpu.br @final - - cpu::cpu.label @outcome_zero - cpu::cpu.br @final - - cpu::cpu.label @final - cpu::cpu.const u64, 1 - circuit::circuit.measure - cpu::cpu.halt -} - -fn @measure_q0() { - cpu::cpu.const u64, 0 - circuit::circuit.measure - // Stack: [outcome] - cpu::cpu.ret 1 -} +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "measure_q0", "function_call_branch_both.sst", "lost", "kept", "outcome_one", "outcome_zero", "final"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 23, + file: 2, + }, + { + name: 1, // measure_q0 + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 23, + end_address: 26, + file: 2, + }, + ], + labels: [ + { name: 3, address: 10 }, // lost + { name: 4, address: 13 }, // kept + { name: 5, address: 16 }, // outcome_one + { name: 6, address: 19 }, // outcome_zero + { name: 7, address: 20 }, // final + ], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + // Exercises a helper that returns the tri-state measurement outcome via + // `ret 1`, with main branching on loss first, then on the 0/1 outcome. + // + // Layout after `call 0, @measure_q0` returns: stack = [outcome] where + // outcome ∈ {0, 1, 2} (2 = Lost). + // + // Loss prob is 0.5, so: + // P(lost) = 0.5 → q1 flipped to |1> → m1 = 1 + // P(kept ∧ outcome = 1) = 0.25 → q1 flipped to |1> → m1 = 1 + // P(kept ∧ outcome = 0) = 0.25 → q1 stays in |0> → m1 = 0 + // → P(m1 = 1) = 0.75. + + fn @main() { + execution::program::const_u64 0 + circuit::circuit::h + + execution::program::const_u64 0 + execution::program::const_f64 0.5 + circuit::circuit::loss + + execution::program::call 0, @measure_q0 + + // Stack: [outcome]. Branch on Lost (outcome == 2) first. + execution::program::dup + execution::program::const_u32 2 + cpu::cpu::eq_u32 + execution::program::cond_br @lost, @kept + + label @lost + // is_lost = true: outcome is meaningless. Flip q1 to mark "lost" + // path. The leftover `outcome` value on the stack is harmless because + // we halt at @final without reading it. + execution::program::const_u64 1 + circuit::circuit::x + execution::program::br @final + + label @kept + // Stack: [outcome (0 or 1)]. Compare to 1 to derive a bool. + execution::program::const_u32 1 + cpu::cpu::eq_u32 + execution::program::cond_br @outcome_one, @outcome_zero + + label @outcome_one + execution::program::const_u64 1 + circuit::circuit::x + execution::program::br @final + + label @outcome_zero + execution::program::br @final + + label @final + execution::program::const_u64 1 + circuit::circuit::measure + execution::program::halt + } + + fn @measure_q0() { + execution::program::const_u64 0 + circuit::circuit::measure + // Stack: [outcome] + execution::program::ret 1 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index 98e96f7eb..2726fef14 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -1,39 +1,76 @@ -device circuit.n_qubits 2; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "measure_q1", "function_call_ret.sst", "one", "zero", "done"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 11, + file: 2, + }, + { + name: 1, // measure_q1 + signature: { params: [], ret: [u32] }, + local_count: 0, + start_address: 11, + end_address: 16, + file: 2, + }, + ], + labels: [ + { name: 3, address: 6 }, // one + { name: 4, address: 9 }, // zero + { name: 5, address: 10 }, // done + ], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Put q0 into |+>. + execution::program::const_u64 0 + circuit::circuit::h + // Measure q1 via a helper that returns the outcome on top of the stack. + execution::program::call 0, @measure_q1 -fn @main() { - // Put q0 into |+>. - cpu::cpu.const u64, 0 - circuit::circuit.h + // Stack: [outcome]. Compare to 1 to derive a bool for cond_br. + execution::program::const_u32 1 + cpu::cpu::eq_u32 + execution::program::cond_br @one, @zero - // Measure q1 via a helper that returns the outcome on top of the stack. - cpu::cpu.call 0, measure_q1 + label @one + // outcome was 1: apply X to q0 as a correction. + execution::program::const_u64 0 + circuit::circuit::x + execution::program::br @done - // Stack: [outcome]. Compare to 1 to derive a bool for cond_br. - cpu::cpu.const u32, 1 - cpu::cpu.eq u32 - cpu::cpu.cond_br @one, @zero + label @zero + execution::program::br @done - cpu::cpu.label @one - // outcome was 1: apply X to q0 as a correction. - cpu::cpu.const u64, 0 - circuit::circuit.x - cpu::cpu.br @done + label @done + execution::program::ret 0 + } - cpu::cpu.label @zero - cpu::cpu.br @done + fn @measure_q1() -> u32 { + execution::program::const_u64 1 + circuit::circuit::h - cpu::cpu.label @done - cpu::cpu.ret 0 -} + execution::program::const_u64 1 + circuit::circuit::measure + // Stack: [outcome] -fn @measure_q1() -> u32 { - cpu::cpu.const u64, 1 - circuit::circuit.h - - cpu::cpu.const u64, 1 - circuit::circuit.measure - // Stack: [outcome] - - cpu::cpu.ret 1 -} + execution::program::ret 1 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index dd1717db7..ec4615220 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -1,16 +1,42 @@ -device circuit.n_qubits 2; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "hello_circuit.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 9, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + execution::program::const_u64 0 + circuit::circuit::h -fn @main() { - cpu::cpu.const u64, 0 - circuit::circuit.h + execution::program::const_u64 0 + execution::program::const_u64 1 + circuit::circuit::cnot - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot + execution::program::const_u64 0 + execution::program::const_f64 0.1 + circuit::circuit::rx - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 0.1 - circuit::circuit.rx - - cpu::cpu.ret 0 -} + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst index 2b5f7557e..919936a05 100644 --- a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst +++ b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst @@ -1,28 +1,54 @@ -device circuit.n_qubits 2; -device circuit.backend lossy_paulisum; -device circuit.observable ZZ; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + circuit.backend lossy_paulisum; + circuit.observable ZZ; + metadata { + strings: ["main", "lossy_paulisum_loss_trace.sst", "Z?*"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 10, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + // LossyPauliSum end-to-end with a single-qubit loss channel. The header + // seeds `ZZ`, the body Heisenberg-propagates ZZ backward through CNOT(0,1) + // to Z_1 (= IZ), then applies loss(q0, p=0.3). The pattern `Z?*` matches + // all Z/I-only Paulis on the surviving qubits, so the trace picks up the + // IZ coefficient that survived the loss channel. + // + // The Rust-side test cross-checks this against a pure-Rust LossyPauliSum + // reference with the same Config/strategy, so we don't have to commit to + // the exact loss-channel value — only that .sst and direct API agree. + fn @main() { + execution::program::const_u64 0 + execution::program::const_u64 1 + circuit::circuit::cnot -// LossyPauliSum end-to-end with a single-qubit loss channel. The header -// seeds `ZZ`, the body Heisenberg-propagates ZZ backward through CNOT(0,1) -// to Z_1 (= IZ), then applies loss(q0, p=0.3). The pattern `Z?*` matches -// all Z/I-only Paulis on the surviving qubits, so the trace picks up the -// IZ coefficient that survived the loss channel. -// -// The Rust-side test cross-checks this against a pure-Rust LossyPauliSum -// reference with the same Config/strategy, so we don't have to commit to -// the exact loss-channel value — only that .sst and direct API agree. -fn @main() { - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot + execution::program::const_u64 0 + execution::program::const_f64 0.3 + circuit::circuit::loss - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 0.3 - circuit::circuit.loss + circuit::circuit::truncate - circuit::circuit.truncate - - cpu::cpu.const str, "Z?*" - circuit::circuit.trace - cpu::cpu.ret 0 -} + execution::program::const_string "Z?*" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst index 42246add1..fcdea982e 100644 --- a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst @@ -1,18 +1,44 @@ -device circuit.n_qubits 2; -device circuit.backend paulisum; -device circuit.observable ZZ; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + circuit.backend paulisum; + circuit.observable ZZ; + metadata { + strings: ["main", "paulisum_bell_trace.sst", "Z?*"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 8, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Textbook H(0); CNOT(0,1) — emit reversed for Heisenberg propagation. + execution::program::const_u64 0 + execution::program::const_u64 1 + circuit::circuit::cnot -fn @main() { - // Textbook H(0); CNOT(0,1) — emit reversed for Heisenberg propagation. - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot + execution::program::const_u64 0 + circuit::circuit::h - cpu::cpu.const u64, 0 - circuit::circuit.h - - // Trace against |00>: match Z-or-identity on every qubit. - cpu::cpu.const str, "Z?*" - circuit::circuit.trace - cpu::cpu.ret 0 -} + // Trace against |00>: match Z-or-identity on every qubit. + execution::program::const_string "Z?*" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst index 7e2683493..eb7964409 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst @@ -1,27 +1,53 @@ -device circuit.n_qubits 3; -device circuit.backend paulisum; -device circuit.observable XXX; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 3; + circuit.backend paulisum; + circuit.observable XXX; + metadata { + strings: ["main", "paulisum_ghz_xxx_trace.sst", "Z?*"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 11, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Heisenberg backward through GHZ prep: forward is H(0); CNOT(0,1); CNOT(1,2), + // so we apply CNOT(1,2); CNOT(0,1); H(0) here. + // + // XXX --CNOT(1,2)--> XXI (X_2 on target absorbs into X_1 on control) + // --CNOT(0,1)--> XII + // --H(0)------> ZII + // Trace against Z/I-only Paulis picks up the coefficient of ZII = 1.0. + execution::program::const_u64 1 + execution::program::const_u64 2 + circuit::circuit::cnot -fn @main() { - // Heisenberg backward through GHZ prep: forward is H(0); CNOT(0,1); CNOT(1,2), - // so we apply CNOT(1,2); CNOT(0,1); H(0) here. - // - // XXX --CNOT(1,2)--> XXI (X_2 on target absorbs into X_1 on control) - // --CNOT(0,1)--> XII - // --H(0)------> ZII - // Trace against Z/I-only Paulis picks up the coefficient of ZII = 1.0. - cpu::cpu.const u64, 1 - cpu::cpu.const u64, 2 - circuit::circuit.cnot + execution::program::const_u64 0 + execution::program::const_u64 1 + circuit::circuit::cnot - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot + execution::program::const_u64 0 + circuit::circuit::h - cpu::cpu.const u64, 0 - circuit::circuit.h - - cpu::cpu.const str, "Z?*" - circuit::circuit.trace - cpu::cpu.ret 0 -} + execution::program::const_string "Z?*" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/paulisum_leakage_error.sst b/crates/ppvm-vihaco/tests/paulisum_leakage_error.sst index 1e69b1bae..e2073fab1 100644 --- a/crates/ppvm-vihaco/tests/paulisum_leakage_error.sst +++ b/crates/ppvm-vihaco/tests/paulisum_leakage_error.sst @@ -1,13 +1,39 @@ -device circuit.n_qubits 1; -device circuit.backend paulisum; -device circuit.observable Z; - -// Leakage is GeneralizedTableau-only. PauliSum has no leakage_channel, -// so this program must fail at execute with a backend-rejection error. -fn @main() { - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 0.0 - cpu::cpu.const f64, 1.0 - circuit::circuit.leakage - cpu::cpu.ret 0 -} +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + circuit.backend paulisum; + circuit.observable Z; + metadata { + strings: ["main", "paulisum_leakage_error.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 5, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + // Leakage is GeneralizedTableau-only. PauliSum has no leakage_channel, + // so this program must fail at execute with a backend-rejection error. + fn @main() { + execution::program::const_u64 0 + execution::program::const_f64 0.0 + execution::program::const_f64 1.0 + circuit::circuit::leakage + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst index a6ebe6990..f85d5b040 100644 --- a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst +++ b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst @@ -1,9 +1,35 @@ -device circuit.n_qubits 1; -device circuit.backend paulisum; -device circuit.observable Z; - -fn @main() { - cpu::cpu.const u64, 0 - circuit::circuit.measure - cpu::cpu.ret 0 -} +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + circuit.backend paulisum; + circuit.observable Z; + metadata { + strings: ["main", "paulisum_measure_error.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 3, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + execution::program::const_u64 0 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst index 40fb69e01..df2a7cda2 100644 --- a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst @@ -1,11 +1,37 @@ -device circuit.n_qubits 2; -device circuit.backend paulisum; -device circuit.observable 1.0*ZZ+0.5*XX; - -fn @main() { - // No gates — the multi-term observable seeds the state directly. - // Pattern `[XZ]?*` matches both ZZ (coef 1.0) and XX (coef 0.5). - cpu::cpu.const str, "[XZ]?*" - circuit::circuit.trace - cpu::cpu.ret 0 -} +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + circuit.backend paulisum; + circuit.observable 1.0*ZZ+0.5*XX; + metadata { + strings: ["main", "paulisum_multi_term_trace.sst", "[XZ]?*"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 3, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // No gates — the multi-term observable seeds the state directly. + // Pattern `[XZ]?*` matches both ZZ (coef 1.0) and XX (coef 0.5). + execution::program::const_string "[XZ]?*" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst index 69bf559eb..649b4a39b 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst @@ -1,17 +1,43 @@ -device circuit.n_qubits 1; -device circuit.backend paulisum; -device circuit.observable Z; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + circuit.backend paulisum; + circuit.observable Z; + metadata { + strings: ["main", "paulisum_ry_z_trace.sst", "Z?*"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 6, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // PauliSum already runs in the Heisenberg picture: `circuit.ry` performs + // RY(θ)† Z RY(θ) = cos(θ)·Z + sin(θ)·X. A single gate means no + // reversal is needed beyond that. Trace against Z/I-only Paulis picks + // up the cos(θ) coefficient on Z; the sin(θ)·X term contributes 0. + execution::program::const_u64 0 + execution::program::const_f64 0.7 + circuit::circuit::ry -fn @main() { - // PauliSum already runs in the Heisenberg picture: `circuit.ry` performs - // RY(θ)† Z RY(θ) = cos(θ)·Z + sin(θ)·X. A single gate means no - // reversal is needed beyond that. Trace against Z/I-only Paulis picks - // up the cos(θ) coefficient on Z; the sin(θ)·X term contributes 0. - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 0.7 - circuit::circuit.ry - - cpu::cpu.const str, "Z?*" - circuit::circuit.trace - cpu::cpu.ret 0 -} + execution::program::const_string "Z?*" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst index 7c4f7d5da..be0616e86 100644 --- a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst +++ b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst @@ -1,37 +1,63 @@ -device circuit.n_qubits 2; -device circuit.backend paulisum; -device circuit.observable ZZ; -device circuit.coefficient_threshold 1e-6; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + circuit.backend paulisum; + circuit.observable ZZ; + circuit.coefficient_threshold 1e-6; + metadata { + strings: ["main", "paulisum_trotter_truncate.sst", "Z?*"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 21, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Two Trotter layers of RXX(0.1) RZZ(0.05), with explicit truncate + // between layers. RXX branches Z->Y so the sum grows; truncate prunes + // small-coefficient terms via `coefficient_threshold`. + execution::program::const_u64 0 + execution::program::const_u64 1 + execution::program::const_f64 0.1 + circuit::circuit::rxx -fn @main() { - // Two Trotter layers of RXX(0.1) RZZ(0.05), with explicit truncate - // between layers. RXX branches Z->Y so the sum grows; truncate prunes - // small-coefficient terms via `coefficient_threshold`. - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - cpu::cpu.const f64, 0.1 - circuit::circuit.rxx + execution::program::const_u64 0 + execution::program::const_u64 1 + execution::program::const_f64 0.05 + circuit::circuit::rzz - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - cpu::cpu.const f64, 0.05 - circuit::circuit.rzz + circuit::circuit::truncate - circuit::circuit.truncate + execution::program::const_u64 0 + execution::program::const_u64 1 + execution::program::const_f64 0.1 + circuit::circuit::rxx - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - cpu::cpu.const f64, 0.1 - circuit::circuit.rxx + execution::program::const_u64 0 + execution::program::const_u64 1 + execution::program::const_f64 0.05 + circuit::circuit::rzz - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - cpu::cpu.const f64, 0.05 - circuit::circuit.rzz + circuit::circuit::truncate - circuit::circuit.truncate - - cpu::cpu.const str, "Z?*" - circuit::circuit.trace - cpu::cpu.ret 0 -} + execution::program::const_string "Z?*" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/rotxy.sst b/crates/ppvm-vihaco/tests/rotxy.sst index 54acb6255..1eb3858b9 100644 --- a/crates/ppvm-vihaco/tests/rotxy.sst +++ b/crates/ppvm-vihaco/tests/rotxy.sst @@ -1,15 +1,41 @@ -device circuit.n_qubits 1; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "rotxy.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 7, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // R(axis_angle = π/2, θ = π) == RY(π), so |0> is sent to |1>. + // Stack order for `circuit.r`: qubit, then axis_angle, then theta. + execution::program::const_u64 0 + execution::program::const_f64 1.5707963267948966 + execution::program::const_f64 3.141592653589793 + circuit::circuit::r -fn @main() { - // R(axis_angle = π/2, θ = π) == RY(π), so |0> is sent to |1>. - // Stack order for `circuit.r`: qubit, then axis_angle, then theta. - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 1.5707963267948966 - cpu::cpu.const f64, 3.141592653589793 - circuit::circuit.r + execution::program::const_u64 0 + circuit::circuit::measure - cpu::cpu.const u64, 0 - circuit::circuit.measure - - cpu::cpu.ret 0 -} + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 755a2dbc7..9fb42e46a 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -1,12 +1,25 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! End-to-end fixture coverage: parse + resolve + run each `.sst` file in -//! this directory via the public `PPVM` API. +//! End-to-end fixture coverage: compile + run each `.sst` file in this +//! directory via the public `PPVM` API. -use ppvm_vihaco::composite::PPVM; +use ppvm_vihaco::composite::{PPVM, PPVMModule}; +use ppvm_vihaco::load_module_file; use ppvm_vihaco::measurements::MeasurementOutcome; +/// Run a compiled fixture on a fresh machine for its declared device. +fn run_module(module: &PPVMModule) -> eyre::Result { + let mut machine = PPVM::new(&module.extra, None)?; + vihaco::execute(module, &mut machine, &[])?; + Ok(machine) +} + +/// Compile and run a fixture file. +fn run_file(path: &str) -> eyre::Result { + run_module(&load_module_file(path)?) +} + // TODO: bytecode (.ssb) is disabled until vihaco ships codecs for its own dialects. // /// Dump a fixture to a `.ssb` file, load it back, and run it. Exercises the // /// full bytecode round-trip through disk: `dump_file` → `load_bytecode_file`. @@ -27,18 +40,13 @@ use ppvm_vihaco::measurements::MeasurementOutcome; #[test] fn bell_sst_runs_and_records_two_measurements() { - let mut machine = PPVM::default(); - machine - .run_file("tests/bell.sst") - .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); + let machine = run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); assert_eq!(machine.measurement_record().len(), 2); } #[test] fn hello_circuit_sst_parses_and_runs() { - let mut machine = PPVM::default(); - machine - .run_file("tests/hello_circuit.sst") + let machine = run_file("tests/hello_circuit.sst") .unwrap_or_else(|e| panic!("run hello_circuit.sst: {e:?}")); // hello_circuit.sst applies H + CNOT + RX(0.1); no measurements. assert_eq!(machine.measurement_record().len(), 0); @@ -46,8 +54,8 @@ fn hello_circuit_sst_parses_and_runs() { #[test] fn arithmetic_sst_uses_lhs_then_rhs_order() { - let machine = ppvm_vihaco::run_file("tests/arithmetic.sst") - .unwrap_or_else(|e| panic!("run arithmetic.sst: {e:?}")); + let machine = + run_file("tests/arithmetic.sst").unwrap_or_else(|e| panic!("run arithmetic.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 1, "expected exactly one measurement"); assert_eq!( @@ -62,8 +70,7 @@ fn rotxy_sst_runs_and_flips_qubit() { // `rotxy.sst` applies R(axis_angle=π/2, θ=π) = RY(π) to q0, deterministically // sending |0> → |1>, then measures it. Exercises the `circuit::circuit.r` path end to // end: parse → resolve (pop θ, axis_angle, qubit) → execute via `tab.r`. - let machine = - ppvm_vihaco::run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); + let machine = run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 1, "expected exactly one measurement"); assert_eq!( @@ -88,7 +95,7 @@ fn rotxy_sst_runs_and_flips_qubit() { fn tableau_leakage_pins_to_one_and_skips_x() { // `tableau_leakage.sst` leaks q0 with (p0, p1) = (0, 1), pinning |1⟩, // then applies X. A leaked qubit is frozen, so measure still returns 1. - let machine = ppvm_vihaco::run_file("tests/tableau_leakage.sst") + let machine = run_file("tests/tableau_leakage.sst") .unwrap_or_else(|e| panic!("run tableau_leakage.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 1, "expected exactly one measurement"); @@ -110,8 +117,7 @@ fn tableau_leakage_pins_to_one_and_skips_x() { #[test] fn run_file_via_library_helper() { - let machine = - ppvm_vihaco::run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); + let machine = run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); assert_eq!(machine.measurement_record().len(), 2); } @@ -120,7 +126,7 @@ fn function_call_jumps_into_callee_body() { // `function_call.sst` has main `call` into `@run_circuit`, which puts q1 // in |+>, measures it, and `halt`s. Verifies CallPatch resolves the // symbolic target and op_call actually transfers control there. - let machine = ppvm_vihaco::run_file("tests/function_call.sst") + let machine = run_file("tests/function_call.sst") .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 1, "expected exactly one measurement"); @@ -133,7 +139,7 @@ fn function_call_jumps_into_callee_body() { #[test] fn function_call_returns() { - let machine = ppvm_vihaco::run_file("tests/function_call_ret.sst") + let machine = run_file("tests/function_call_ret.sst") .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 1, "expected exactly one measurement"); @@ -149,7 +155,7 @@ fn branch_on_outcome_deterministic_x_path() { // `branch_on_outcome_x.sst` applies X to q0 instead of H, so the outcome // is deterministically 1. The cpu::cpu.cond_br must therefore take the @one path, // which flips q1 before measuring it, yielding m1 = 1 as well. - let machine = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") + let machine = run_file("tests/branch_on_outcome_x.sst") .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 2, "expected exactly two measurements"); @@ -173,7 +179,7 @@ fn branch_on_outcome_statistics_balanced_and_invariant_holds() { const SHOTS: usize = 400; let mut ones = 0usize; for _ in 0..SHOTS { - let machine = ppvm_vihaco::run_file("tests/branch_on_outcome.sst") + let machine = run_file("tests/branch_on_outcome.sst") .unwrap_or_else(|e| panic!("run branch_on_outcome.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 2); @@ -212,7 +218,7 @@ fn function_call_branch_on_both_returned_values() { let mut q0_lost = 0usize; let mut q1_ones = 0usize; for _ in 0..SHOTS { - let machine = ppvm_vihaco::run_file("tests/function_call_branch_both.sst") + let machine = run_file("tests/function_call_branch_both.sst") .unwrap_or_else(|e| panic!("run function_call_branch_both.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 2, "expected exactly two measurements"); @@ -246,7 +252,7 @@ fn paulisum_bell_zz_trace_through_sst() { // Conjugating ZZ by CNOT(0,1) gives Z_1 (= IZ); H on q0 leaves IZ // untouched. Tracing against |00> matches IZ (pattern `Z?*`) and // returns +1.0 — matching ⟨Φ+|ZZ|Φ+⟩ = 1. - let machine = ppvm_vihaco::run_file("tests/paulisum_bell_trace.sst") + let machine = run_file("tests/paulisum_bell_trace.sst") .unwrap_or_else(|e| panic!("run paulisum_bell_trace.sst: {e:?}")); let trace = machine.trace_record(); assert_eq!(trace.len(), 1, "expected one trace emission"); @@ -263,7 +269,7 @@ fn paulisum_multi_term_observable_trace_through_sst() { // header, seeds both terms, and an end-of-program trace picks up their // contributions. With no gates applied, tracing `[XZ]?*` matches both // ZZ and XX directly → coef sum = 1.0 + 0.5 = 1.5. - let machine = ppvm_vihaco::run_file("tests/paulisum_multi_term_trace.sst") + let machine = run_file("tests/paulisum_multi_term_trace.sst") .unwrap_or_else(|e| panic!("run paulisum_multi_term_trace.sst: {e:?}")); let trace = machine.trace_record(); assert_eq!(trace.len(), 1); @@ -282,7 +288,7 @@ fn paulisum_trotter_matches_pure_rust_reference() { // gives deterministic iteration order (Decision 7), so truncation order // and float accumulation are stable across both paths. - let machine = ppvm_vihaco::run_file("tests/paulisum_trotter_truncate.sst") + let machine = run_file("tests/paulisum_trotter_truncate.sst") .unwrap_or_else(|e| panic!("run paulisum_trotter_truncate.sst: {e:?}")); let through_sst = machine.trace_record(); assert_eq!(through_sst.len(), 1, "expected one trace emission"); @@ -323,7 +329,7 @@ fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { // then loss(q0, 0.3), then traces. The .sst-driven trace must agree // bit-for-bit with a pure-Rust reference using the same Config (a // `LossyPauliSumConfig<8>` matching the Bits64 bucket) and operations. - let machine = ppvm_vihaco::run_file("tests/lossy_paulisum_loss_trace.sst") + let machine = run_file("tests/lossy_paulisum_loss_trace.sst") .unwrap_or_else(|e| panic!("run lossy_paulisum_loss_trace.sst: {e:?}")); let through_sst = machine.trace_record(); assert_eq!(through_sst.len(), 1, "expected one trace emission"); @@ -361,11 +367,9 @@ fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { fn paulisum_measure_returns_unsupported_error() { // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a // clear "not supported on the PauliSum backend" error. - let mut machine = PPVM::default(); - machine - .load_file("tests/paulisum_measure_error.sst") + let module = load_module_file("tests/paulisum_measure_error.sst") .unwrap_or_else(|e| panic!("load paulisum_measure_error.sst: {e:?}")); - let err = machine.run().unwrap_err(); + let err = run_module(&module).err().expect("run should fail"); let msg = err.to_string(); assert!( msg.contains("not supported on the PauliSum backend"), @@ -377,11 +381,9 @@ fn paulisum_measure_returns_unsupported_error() { fn paulisum_leakage_returns_unsupported_error() { // Leakage is tableau-only. PauliSum must reject it the same way it // rejects Measure — not with a mismatched-argument fallback. - let mut machine = PPVM::default(); - machine - .load_file("tests/paulisum_leakage_error.sst") + let module = load_module_file("tests/paulisum_leakage_error.sst") .unwrap_or_else(|e| panic!("load paulisum_leakage_error.sst: {e:?}")); - let err = machine.run().unwrap_err(); + let err = run_module(&module).err().expect("run should fail"); let msg = err.to_string(); assert!( msg.contains("not supported on the PauliSum backend"), @@ -396,7 +398,7 @@ fn tableau_bell_zz_trace_through_sst() { // Bell-state ⟨ZZ⟩ via the Tableau backend's `Trace` instruction: // forward H(0); CNOT(0, 1) leaves |ψ⟩ = |Φ+⟩, and `Z0Z1` matches exactly // the Pauli word ZZ, so `tab.trace(&pat) = ⟨Φ+|ZZ|Φ+⟩ = 1`. - let machine = ppvm_vihaco::run_file("tests/tableau_bell_trace.sst") + let machine = run_file("tests/tableau_bell_trace.sst") .unwrap_or_else(|e| panic!("run tableau_bell_trace.sst: {e:?}")); let trace = machine.trace_record(); assert_eq!(trace.len(), 1, "expected one trace emission"); @@ -408,10 +410,8 @@ fn tableau_bell_zz_trace_through_sst() { } fn assert_cross_backend_agreement(tableau_sst: &str, paulisum_sst: &str) { - let tab = - ppvm_vihaco::run_file(tableau_sst).unwrap_or_else(|e| panic!("run {tableau_sst}: {e:?}")); - let ps = - ppvm_vihaco::run_file(paulisum_sst).unwrap_or_else(|e| panic!("run {paulisum_sst}: {e:?}")); + let tab = run_file(tableau_sst).unwrap_or_else(|e| panic!("run {tableau_sst}: {e:?}")); + let ps = run_file(paulisum_sst).unwrap_or_else(|e| panic!("run {paulisum_sst}: {e:?}")); let tab_v = tab.trace_record(); let ps_v = ps.trace_record(); assert_eq!(tab_v.len(), 1, "{tableau_sst}: expected one trace emission"); @@ -492,7 +492,7 @@ fn tableau_and_paulisum_agree_on_ry_z_trace() { // // Use the deterministic X-prepared fixture: q0 measures 1, the branch // // flips q1, so both routes must yield exactly [1], [1]. Any divergence — // // or a binary file mis-parsed as text — fails loudly. -// let from_text = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") +// let from_text = run_file("tests/branch_on_outcome_x.sst") // .unwrap_or_else(|e| panic!("run .sst via load_file: {e:?}")); // // // Dump the same fixture to a `.ssb` and run *that file* through the same @@ -502,7 +502,7 @@ fn tableau_and_paulisum_agree_on_ry_z_trace() { // let out = out.to_str().expect("utf-8 temp path"); // ppvm_vihaco::dump_file("tests/branch_on_outcome_x.sst", out) // .unwrap_or_else(|e| panic!("dump: {e:?}")); -// let from_binary = ppvm_vihaco::run_file(out).unwrap_or_else(|e| panic!("run .ssb: {e:?}")); +// let from_binary = run_file(out).unwrap_or_else(|e| panic!("run .ssb: {e:?}")); // let _ = std::fs::remove_file(out); // // for (label, machine) in [("text", &from_text), ("binary", &from_binary)] { diff --git a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst index 795928243..dd901f32f 100644 --- a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst @@ -1,18 +1,44 @@ -device circuit.n_qubits 2; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 2; + metadata { + strings: ["main", "tableau_bell_trace.sst", "Z0Z1"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 8, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Forward Bell prep: H(0); CNOT(0, 1) → |Φ+⟩. + execution::program::const_u64 0 + circuit::circuit::h -fn @main() { - // Forward Bell prep: H(0); CNOT(0, 1) → |Φ+⟩. - cpu::cpu.const u64, 0 - circuit::circuit.h + execution::program::const_u64 0 + execution::program::const_u64 1 + circuit::circuit::cnot - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot - - // Tableau-side trace: Σ_{P matches pat} ⟨ψ|P|ψ⟩. Positional `Z0Z1` matches - // exactly the ZZ word, so this returns ⟨Φ+|ZZ|Φ+⟩ = 1.0 — the same value - // the PauliSum backend produces by Heisenberg-propagating ZZ backward. - cpu::cpu.const str, "Z0Z1" - circuit::circuit.trace - cpu::cpu.ret 0 -} + // Tableau-side trace: Σ_{P matches pat} ⟨ψ|P|ψ⟩. Positional `Z0Z1` matches + // exactly the ZZ word, so this returns ⟨Φ+|ZZ|Φ+⟩ = 1.0 — the same value + // the PauliSum backend produces by Heisenberg-propagating ZZ backward. + execution::program::const_string "Z0Z1" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst index a2edcb1dd..dbc7d700e 100644 --- a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst @@ -1,20 +1,46 @@ -device circuit.n_qubits 3; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 3; + metadata { + strings: ["main", "tableau_ghz_xxx_trace.sst", "X{3}"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 11, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Forward GHZ prep: H(0); CNOT(0, 1); CNOT(1, 2) → (|000⟩+|111⟩)/√2. + execution::program::const_u64 0 + circuit::circuit::h -fn @main() { - // Forward GHZ prep: H(0); CNOT(0, 1); CNOT(1, 2) → (|000⟩+|111⟩)/√2. - cpu::cpu.const u64, 0 - circuit::circuit.h + execution::program::const_u64 0 + execution::program::const_u64 1 + circuit::circuit::cnot - cpu::cpu.const u64, 0 - cpu::cpu.const u64, 1 - circuit::circuit.cnot + execution::program::const_u64 1 + execution::program::const_u64 2 + circuit::circuit::cnot - cpu::cpu.const u64, 1 - cpu::cpu.const u64, 2 - circuit::circuit.cnot - - // `X{3}` matches exactly the Pauli word XXX. ⟨GHZ|XXX|GHZ⟩ = 1. - cpu::cpu.const str, "X{3}" - circuit::circuit.trace - cpu::cpu.ret 0 -} + // `X{3}` matches exactly the Pauli word XXX. ⟨GHZ|XXX|GHZ⟩ = 1. + execution::program::const_string "X{3}" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/tableau_leakage.sst b/crates/ppvm-vihaco/tests/tableau_leakage.sst index cdb75f94e..fbc65179e 100644 --- a/crates/ppvm-vihaco/tests/tableau_leakage.sst +++ b/crates/ppvm-vihaco/tests/tableau_leakage.sst @@ -1,20 +1,46 @@ -device circuit.n_qubits 1; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "tableau_leakage.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 9, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + // Deterministic leakage: p0=0, p1=1 pins q0 to |1⟩ and marks it leaked. + // Stack order for `circuit.leakage`: qubit, then p0, then p1. + // A subsequent X is a no-op on a leaked qubit, so measurement still + // returns 1 (the pinned bit, not LOST). + fn @main() { + execution::program::const_u64 0 + execution::program::const_f64 0.0 + execution::program::const_f64 1.0 + circuit::circuit::leakage -// Deterministic leakage: p0=0, p1=1 pins q0 to |1⟩ and marks it leaked. -// Stack order for `circuit.leakage`: qubit, then p0, then p1. -// A subsequent X is a no-op on a leaked qubit, so measurement still -// returns 1 (the pinned bit, not LOST). -fn @main() { - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 0.0 - cpu::cpu.const f64, 1.0 - circuit::circuit.leakage + execution::program::const_u64 0 + circuit::circuit::x - cpu::cpu.const u64, 0 - circuit::circuit.x + execution::program::const_u64 0 + circuit::circuit::measure - cpu::cpu.const u64, 0 - circuit::circuit.measure - - cpu::cpu.ret 0 -} + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst index 913543115..5c91b0c6c 100644 --- a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst @@ -1,13 +1,39 @@ -device circuit.n_qubits 1; +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "tableau_ry_z_trace.sst", "Z{1}"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 6, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // RY(θ)|0⟩ = cos(θ/2)|0⟩ + sin(θ/2)|1⟩, so ⟨ψ|Z|ψ⟩ = cos(θ). + // Hard-coded θ = 0.7 → cos(0.7) ≈ 0.7648421872844885. + execution::program::const_u64 0 + execution::program::const_f64 0.7 + circuit::circuit::ry -fn @main() { - // RY(θ)|0⟩ = cos(θ/2)|0⟩ + sin(θ/2)|1⟩, so ⟨ψ|Z|ψ⟩ = cos(θ). - // Hard-coded θ = 0.7 → cos(0.7) ≈ 0.7648421872844885. - cpu::cpu.const u64, 0 - cpu::cpu.const f64, 0.7 - circuit::circuit.ry - - cpu::cpu.const str, "Z{1}" - circuit::circuit.trace - cpu::cpu.ret 0 -} + execution::program::const_string "Z{1}" + circuit::circuit::trace + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). From ed8713a4840b289749a7ae8d1cd01dbeea0d46a8 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 16:27:08 +0100 Subject: [PATCH 28/36] Fix: push measurement and trace effects onto stack --- crates/ppvm-vihaco/src/composite.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 1a130f07f..033183b0d 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -7,8 +7,8 @@ use crate::measurements::{ }; use eyre; use vihaco::{ - BuildMessage, Effects, Execute, ExecutionState, HasInstructionSet, Step, Type, Value, - composite, field, module::LocalModule, + Apply, BuildMessage, Effects, Execute, ExecutionState, HasInstructionSet, PushToStack, Step, + Type, Value, composite, field, module::LocalModule, }; // Device configuration lives in `device_info` (shipped with the circuit @@ -133,9 +133,15 @@ impl vihaco::Apply> for PPVM { for effect in effects { match effect { CircuitOutcomeEffect::Measurement(m) => { + // Programs branch on outcomes: push each as u32 (0/1/2, 2 = Lost). + for &outcome in &m.measurement_results { + self.execution.apply(PushToStack((outcome as u32,))); + } self.measurement_record.record.push(m.measurement_results) } CircuitOutcomeEffect::Trace(t) => { + // Programs can consume the expectation value, as with outcomes. + self.execution.apply(PushToStack((t.value,))); self.trace_record.record.push(t.value); } } From 76ec3a2781f2bb7f3b4740ea8093a6d58def17db Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 16:31:48 +0100 Subject: [PATCH 29/36] Fix trace impl --- crates/ppvm-vihaco/src/component/backend.rs | 12 +++++++ crates/ppvm-vihaco/src/component/ops.rs | 24 +++++++++---- crates/vihaco-circuit-isa/src/messages.rs | 40 +++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/crates/ppvm-vihaco/src/component/backend.rs b/crates/ppvm-vihaco/src/component/backend.rs index 2f791eb05..7428934cd 100644 --- a/crates/ppvm-vihaco/src/component/backend.rs +++ b/crates/ppvm-vihaco/src/component/backend.rs @@ -75,6 +75,9 @@ pub trait CircuitBackend { fn truncate(&mut self); + /// `Σ_{P matches pattern} ⟨P⟩` on the current state. + fn trace(&self, pattern: &PauliPattern) -> eyre::Result; + fn reset(&mut self); } @@ -109,6 +112,10 @@ where fn truncate(&mut self) {} + fn trace(&self, pattern: &PauliPattern) -> eyre::Result { + Ok(GeneralizedTableau::trace(self, pattern)) + } + fn reset(&mut self) { self.reset_all(); } @@ -137,6 +144,7 @@ impl> PauliSumBackend { impl CircuitBackend for PauliSumBackend where T: Config, + for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, { type Config = T; type State = PauliSum; @@ -152,4 +160,8 @@ where fn truncate(&mut self) { self.state.truncate(); } + + fn trace(&self, pattern: &PauliPattern) -> eyre::Result { + Ok(Trace::trace(&self.state, pattern)) + } } diff --git a/crates/ppvm-vihaco/src/component/ops.rs b/crates/ppvm-vihaco/src/component/ops.rs index 4cf5e4806..3559a3d3f 100644 --- a/crates/ppvm-vihaco/src/component/ops.rs +++ b/crates/ppvm-vihaco/src/component/ops.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use super::backend::CircuitBackend; -use crate::measurements::{CircuitOutcomeEffect, MeasurementEffect, MeasurementResult}; +use crate::measurements::{ + CircuitOutcomeEffect, MeasurementEffect, MeasurementResult, TraceEffect, +}; use ppvm_tableau::prelude::*; use vihaco::{Effects, PopFromStack}; use vihaco_circuit_isa::circuit; -use vihaco_circuit_isa::messages::{QubitBatch, QubitPairBatch}; +use vihaco_circuit_isa::messages::{QubitBatch, QubitPairBatch, TraceMessage}; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { @@ -408,10 +410,20 @@ impl CircuitOp for circuit::Truncate { } impl CircuitOp for circuit::Trace { - type Effect = (); - type Message = vihaco::NoMessage; + type Effect = Effects; + type Message = TraceMessage; - fn apply(&self, backend: &mut B, _: Self::Message) -> eyre::Result<()> { - unimplemented!("TODO: needs a custom message"); + fn apply( + &self, + backend: &mut B, + msg: Self::Message, + ) -> eyre::Result { + // Parsed per execution, as before; patterns are short. + let pattern = PauliPattern::parse(&msg.pauli_pattern) + .map_err(|e| eyre::eyre!("invalid Pauli pattern `{}`: {e:?}", msg.pauli_pattern))?; + let value = backend.trace(&pattern)?; + Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { + value, + }))) } } diff --git a/crates/vihaco-circuit-isa/src/messages.rs b/crates/vihaco-circuit-isa/src/messages.rs index 5af9ba537..6700f216d 100644 --- a/crates/vihaco-circuit-isa/src/messages.rs +++ b/crates/vihaco-circuit-isa/src/messages.rs @@ -26,10 +26,36 @@ pub struct TwoQubitPauliErrorMsg { pub ps: [f64; 15], } +/// Pauli pattern to trace against, pushed as a `const_string` id. pub struct TraceMessage { pub pauli_pattern: String, } +/// Read access to a program's string table, used to resolve string operands. +pub trait StringTable { + fn string(&self, id: usize) -> eyre::Result<&str>; +} + +impl StringTable for vihaco::module::LocalModule { + fn string(&self, id: usize) -> eyre::Result<&str> { + self.strings + .get(id) + .map(String::as_str) + .ok_or_else(|| eyre::eyre!("unknown string id {id}")) + } +} + +/// Pop a [`TraceMessage`] whose pattern is a `const_string` id into `strings`. +pub fn pop_trace>( + stack: &mut S, + strings: &(impl StringTable + ?Sized), +) -> eyre::Result { + let id = usize::try_from(stack.pop()?)?; + Ok(TraceMessage { + pauli_pattern: strings.string(id)?.to_owned(), + }) +} + /// Fixed-size parameter blocks popped off the operand stack. pub trait PopParams: Sized { fn pop_params>(source: &mut S) -> eyre::Result; @@ -120,3 +146,17 @@ macro_rules! automatic_batch_message { automatic_batch_message!(QubitBatch, pop_qubit_batch); automatic_batch_message!(QubitPairBatch, pop_qubit_pair_batch); + +impl AutomaticMessage for TraceMessage {} + +impl PrepareMessage for TraceMessage +where + C: Composite + Field + HasOperandStack, + C::Child: Execute, + C::Source: PopStack, + Ctx: StringTable + ?Sized, +{ + fn prepare(parent: &mut C, _: &I, strings: &Ctx) -> eyre::Result { + pop_trace(parent.operand_stack(1), strings) + } +} From 16605141791d63a55f906dddce6d516d0c5d77bd Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 16:42:09 +0100 Subject: [PATCH 30/36] Fix tests --- crates/ppvm-vihaco/tests/sst_fixtures.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 9fb42e46a..91fb65cd6 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -365,14 +365,14 @@ fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { #[test] fn paulisum_measure_returns_unsupported_error() { - // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a - // clear "not supported on the PauliSum backend" error. + // Per Decision 11, Measure on PauliSum is rejected with a clear + // "not supported" error rather than a panic. let module = load_module_file("tests/paulisum_measure_error.sst") .unwrap_or_else(|e| panic!("load paulisum_measure_error.sst: {e:?}")); let err = run_module(&module).err().expect("run should fail"); let msg = err.to_string(); assert!( - msg.contains("not supported on the PauliSum backend"), + msg.contains("not supported"), "expected PauliSum-rejection error, got: {msg}" ); } @@ -386,7 +386,7 @@ fn paulisum_leakage_returns_unsupported_error() { let err = run_module(&module).err().expect("run should fail"); let msg = err.to_string(); assert!( - msg.contains("not supported on the PauliSum backend"), + msg.contains("not supported"), "expected PauliSum-rejection error, got: {msg}" ); } From e742099406796471963a649a931da15f836fa0e4 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 9 Nov 2026 16:44:36 +0100 Subject: [PATCH 31/36] Implement two-qubit pauli error --- crates/ppvm-vihaco/src/component/mod.rs | 2 +- crates/ppvm-vihaco/src/component/ops.rs | 12 ++++++---- crates/vihaco-circuit-isa/src/messages.rs | 29 +++++++++++++++++++++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/ppvm-vihaco/src/component/mod.rs b/crates/ppvm-vihaco/src/component/mod.rs index e6d3ab315..c78c19bec 100644 --- a/crates/ppvm-vihaco/src/component/mod.rs +++ b/crates/ppvm-vihaco/src/component/mod.rs @@ -11,7 +11,7 @@ pub use executors::{Circuit, CircuitExecutor, TableauCircuit}; pub use ops::CircuitOp; pub use vihaco_circuit_isa::messages::{ PopParams, QubitBatch, QubitPairBatch, TraceMessage, TwoQubitPauliErrorMsg, pop_qubit_batch, - pop_qubit_pair_batch, + pop_qubit_pair_batch, pop_two_qubit_pauli_error, }; // vihaco resolves `#[device]` children through these generated companions. diff --git a/crates/ppvm-vihaco/src/component/ops.rs b/crates/ppvm-vihaco/src/component/ops.rs index 3559a3d3f..ec523e18e 100644 --- a/crates/ppvm-vihaco/src/component/ops.rs +++ b/crates/ppvm-vihaco/src/component/ops.rs @@ -8,7 +8,9 @@ use crate::measurements::{ use ppvm_tableau::prelude::*; use vihaco::{Effects, PopFromStack}; use vihaco_circuit_isa::circuit; -use vihaco_circuit_isa::messages::{QubitBatch, QubitPairBatch, TraceMessage}; +use vihaco_circuit_isa::messages::{ + QubitBatch, QubitPairBatch, TraceMessage, TwoQubitPauliErrorMsg, +}; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { @@ -359,11 +361,13 @@ impl CircuitOp for circuit::PauliError { impl CircuitOp for circuit::TwoQubitPauliError { type Effect = (); - type Message = PopFromStack<(u64, u64, f64, f64)>; + type Message = TwoQubitPauliErrorMsg; fn apply(&self, backend: &mut B, msg: Self::Message) -> eyre::Result<()> { - // TODO: probably needs a custom message since we can't pop float arrays - unimplemented!("TODO: needs a custom message") + backend + .state() + .two_qubit_pauli_error(msg.q0, msg.q1, msg.ps); + Ok(()) } } diff --git a/crates/vihaco-circuit-isa/src/messages.rs b/crates/vihaco-circuit-isa/src/messages.rs index 6700f216d..bb237b2c9 100644 --- a/crates/vihaco-circuit-isa/src/messages.rs +++ b/crates/vihaco-circuit-isa/src/messages.rs @@ -20,12 +20,24 @@ pub struct QubitPairBatch

{ pub params: P, } +/// Single two-qubit Pauli channel: pushed as `q0 q1`, then the 15 probabilities. pub struct TwoQubitPauliErrorMsg { - pub q0: u64, - pub q1: u64, + pub q0: usize, + pub q1: usize, pub ps: [f64; 15], } +/// Pop a [`TwoQubitPauliErrorMsg`] pushed as `q0 q1`, then the 15 probabilities. +pub fn pop_two_qubit_pauli_error(stack: &mut S) -> eyre::Result +where + S: PopStack + PopStack, +{ + let ps = <[f64; 15]>::pop_params(stack)?; + let q1 = usize::try_from(PopStack::::pop(stack)?)?; + let q0 = usize::try_from(PopStack::::pop(stack)?)?; + Ok(TwoQubitPauliErrorMsg { q0, q1, ps }) +} + /// Pauli pattern to trace against, pushed as a `const_string` id. pub struct TraceMessage { pub pauli_pattern: String, @@ -147,6 +159,19 @@ macro_rules! automatic_batch_message { automatic_batch_message!(QubitBatch, pop_qubit_batch); automatic_batch_message!(QubitPairBatch, pop_qubit_pair_batch); +impl AutomaticMessage for TwoQubitPauliErrorMsg {} + +impl PrepareMessage for TwoQubitPauliErrorMsg +where + C: Composite + Field + HasOperandStack, + C::Child: Execute, + C::Source: PopStack + PopStack, +{ + fn prepare(parent: &mut C, _: &I, _: &Ctx) -> eyre::Result { + pop_two_qubit_pauli_error(parent.operand_stack(17)) + } +} + impl AutomaticMessage for TraceMessage {} impl PrepareMessage for TraceMessage From 7a068b643c02e49fbb1fa4f707d0a10a05ca93c6 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 10 Nov 2026 10:19:35 +0100 Subject: [PATCH 32/36] Rework tui command to send messages --- Cargo.lock | 1 + crates/ppvm-tui/Cargo.toml | 1 + crates/ppvm-tui/src/app.rs | 24 +- crates/ppvm-tui/src/command.rs | 408 +++++++++++------- crates/ppvm-vihaco/src/component/executors.rs | 10 + crates/ppvm-vihaco/src/composite.rs | 60 ++- crates/ppvm-vihaco/src/lib.rs | 5 +- 7 files changed, 339 insertions(+), 170 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04d9be00c..eb6b89a9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2398,6 +2398,7 @@ dependencies = [ "eyre", "ppvm-vihaco", "ratatui", + "vihaco", ] [[package]] diff --git a/crates/ppvm-tui/Cargo.toml b/crates/ppvm-tui/Cargo.toml index 3773bb66c..a0aa43655 100644 --- a/crates/ppvm-tui/Cargo.toml +++ b/crates/ppvm-tui/Cargo.toml @@ -13,3 +13,4 @@ eyre = "0.6.12" # and unifies event types with any host ratatui app. ratatui = "0.29.0" ppvm-vihaco = { version = "0.3.0", path = "../ppvm-vihaco" } +vihaco = "0.7.0" diff --git a/crates/ppvm-tui/src/app.rs b/crates/ppvm-tui/src/app.rs index 58f92c44a..5fbfa9be8 100644 --- a/crates/ppvm-tui/src/app.rs +++ b/crates/ppvm-tui/src/app.rs @@ -10,14 +10,14 @@ use eyre::{Result, eyre}; use ppvm_vihaco::composite::{PPVM, StepOutcome}; use ppvm_vihaco::measurements::MeasurementResult; -use ppvm_vihaco::{CircuitInstruction, PPVMModule, compile_program, load_module_file}; +use ppvm_vihaco::{PPVMModule, compile_program, load_module_file}; use ratatui::Frame; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::widgets::Clear; use crate::codeview::CodeView; -use crate::command::{Command, parse_command}; +use crate::command::{Command, parse_command, send_gate}; use crate::editor::LineEditor; use crate::widgets::{CommandLine, HelpOverlay, ProgramView, RecordView, StateView}; @@ -99,11 +99,7 @@ impl AppState { Ok(()) } Command::Device(n) => self.new_device(n), - Command::Gate { - inst, - qubits, - params, - } => self.apply_gate(inst, &qubits, ¶ms), + Command::Gate { name, args } => self.apply_gate(&name, &args), Command::Step => self.step(), Command::Continue => self.cont(), Command::Reset => self.reset(), @@ -268,18 +264,14 @@ impl AppState { self.paused } - fn apply_gate( - &mut self, - inst: CircuitInstruction, - qubits: &[usize], - params: &[f64], - ) -> Result<()> { + fn apply_gate(&mut self, name: &str, args: &[String]) -> Result<()> { let m = self .machine .as_mut() .ok_or_else(|| eyre!("no device — run `device N` or :load a file first"))?; let before = m.measurement_record().len(); - m.apply_circuit_instruction(inst, qubits, params)?; + let args: Vec<&str> = args.iter().map(String::as_str).collect(); + send_gate(m, name, &args)?; // Any new record entries are this gate's measurement outcomes. let new: Vec = m.measurement_record()[before..].to_vec(); if new.is_empty() { @@ -465,7 +457,8 @@ Meta / debug :quit :q (Ctrl-C) leave Gates (q = qubit index; angles / probabilities are floats) - x y z h s sadj sqrtx sqrty sqrtxadj sqrtyadj t tadj reset measure + x y z h s s_adj t t_adj reset measure + sqrt_x sqrt_y sqrt_x_adj sqrt_y_adj cnot cz rx ry rz r u3 @@ -473,6 +466,7 @@ Gates (q = qubit index; angles / probabilities are floats) depolarize loss

depolarize2

leakage pauli_error correlated_loss + two_qubit_pauli_error .. Line editing: ←/→ move · Home/End · Backspace/Del · ↑/↓ history"; diff --git a/crates/ppvm-tui/src/command.rs b/crates/ppvm-tui/src/command.rs index d3b5732a7..956c621d2 100644 --- a/crates/ppvm-tui/src/command.rs +++ b/crates/ppvm-tui/src/command.rs @@ -2,77 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 //! The TUI command grammar. Bare tokens are gate ops; `:`-prefixed tokens are -//! meta/debug commands; an empty line means "step". The gate table is ported -//! from the removed rustyline REPL. +//! meta/debug commands; an empty line means "step". Gate names are the ISA +//! mnemonics, i.e. the same spelling as `circuit::circuit.` in `.sst`. use eyre::{Result, WrapErr, bail, eyre}; -use ppvm_vihaco::CircuitInstruction; - -/// How a gate command lowers: the engine instruction plus how many qubit and -/// float operands it consumes (qubits first, then floats). -pub struct GateSpec { - pub inst: CircuitInstruction, - pub qubits: usize, - pub floats: usize, -} - -/// Resolve a gate name to its spec, or `None` if it is not a gate. -/// `TwoQubitPauliError` is intentionally absent — its tableau arm is `todo!()`. -pub fn gate_spec(name: &str) -> Option { - use CircuitInstruction::*; - let (inst, qubits, floats) = match name { - "x" => (X, 1, 0), - "y" => (Y, 1, 0), - "z" => (Z, 1, 0), - "h" => (H, 1, 0), - "s" => (S, 1, 0), - "sadj" => (SAdj, 1, 0), - "sqrtx" => (SqrtX, 1, 0), - "sqrty" => (SqrtY, 1, 0), - "sqrtxadj" => (SqrtXAdj, 1, 0), - "sqrtyadj" => (SqrtYAdj, 1, 0), - "t" => (T, 1, 0), - "tadj" => (TAdj, 1, 0), - "measure" => (Measure, 1, 0), - "reset" => (Reset, 1, 0), - "cnot" => (CNOT, 2, 0), - "cz" => (CZ, 2, 0), - "rx" => (RX, 1, 1), - "ry" => (RY, 1, 1), - "rz" => (RZ, 1, 1), - "r" => (R, 1, 2), - "rxx" => (RXX, 2, 1), - "ryy" => (RYY, 2, 1), - "rzz" => (RZZ, 2, 1), - "u3" => (U3, 1, 3), - "depolarize" => (Depolarize, 1, 1), - "depolarize2" => (Depolarize2, 2, 1), - "loss" => (Loss, 1, 1), - "pauli_error" => (PauliError, 1, 3), - "correlated_loss" => (CorrelatedLoss, 2, 3), - // Same shape as `r`: one qubit, two floats. Push order is q, p0, p1 - // (probabilities of leaking into pinned |0⟩ / |1⟩). - "leakage" => (Leakage, 1, 2), - _ => return None, - }; - Some(GateSpec { - inst, - qubits, - floats, - }) -} +use ppvm_vihaco::circuit; +use ppvm_vihaco::component::{CircuitOp, TwoQubitPauliErrorMsg}; +use ppvm_vihaco::composite::{PPVM, Record}; +use vihaco::PopFromStack; /// One parsed command-line entry. #[derive(Debug, PartialEq)] pub enum Command { /// `device N` — (re)create a fresh N-qubit tableau device. Device(usize), - /// A gate op, e.g. `cnot 0 1` or `rx 0 0.5`. - Gate { - inst: CircuitInstruction, - qubits: Vec, - params: Vec, - }, + /// A gate op, e.g. `cnot 0 1` or `rx 0 0.5`; run with [`send_gate`]. + Gate { name: String, args: Vec }, /// Advance one instruction (also the meaning of an empty line). Step, /// Run to the next cpu::cpu.breakpoint or program end. @@ -118,7 +63,7 @@ pub fn parse_command(line: &str) -> Result { // Bare tokens: `device N` or a gate op. let mut it = line.split_whitespace(); let head = it.next().unwrap(); - let args: Vec<&str> = it.collect(); + let args: Vec = it.map(str::to_string).collect(); if head == "device" { if args.len() != 1 { @@ -128,103 +73,210 @@ pub fn parse_command(line: &str) -> Result { return Ok(Command::Device(n)); } - let spec = - gate_spec(head).ok_or_else(|| eyre!("unknown command {head:?}; try :load or device N"))?; - let expected = spec.qubits + spec.floats; - if args.len() != expected { - bail!( - "{head} takes {} qubit(s) and {} param(s), got {}", - spec.qubits, - spec.floats, - args.len() - ); - } - let (qs, ps) = args.split_at(spec.qubits); - let qubits = qs - .iter() - .map(|t| { - t.parse::() - .wrap_err_with(|| format!("invalid qubit index {t:?}")) - }) - .collect::>>()?; - let params = ps - .iter() - .map(|t| { - t.parse::() - .wrap_err_with(|| format!("invalid parameter {t:?}")) - }) - .collect::>>()?; Ok(Command::Gate { - inst: spec.inst, - qubits, - params, + name: head.to_string(), + args, }) } +/// Send gate `name` to the circuit, building its message from `args`. +/// +/// Operands are parsed and range-checked before anything executes, so a +/// malformed command leaves the device untouched. +pub fn send_gate(m: &mut PPVM, name: &str, args: &[&str]) -> Result<()> { + use circuit::*; + match name { + "x" => send(m, name, args, &X), + "y" => send(m, name, args, &Y), + "z" => send(m, name, args, &Z), + "h" => send(m, name, args, &H), + "s" => send(m, name, args, &S), + "s_adj" => send(m, name, args, &SAdj), + "sqrt_x" => send(m, name, args, &SqrtX), + "sqrt_y" => send(m, name, args, &SqrtY), + "sqrt_x_adj" => send(m, name, args, &SqrtXAdj), + "sqrt_y_adj" => send(m, name, args, &SqrtYAdj), + "t" => send(m, name, args, &T), + "t_adj" => send(m, name, args, &TAdj), + "measure" => send(m, name, args, &Measure), + "reset" => send(m, name, args, &Reset), + "cnot" => send(m, name, args, &CNOT), + "cz" => send(m, name, args, &CZ), + "rx" => send(m, name, args, &RX), + "ry" => send(m, name, args, &RY), + "rz" => send(m, name, args, &RZ), + "r" => send(m, name, args, &R), + "rxx" => send(m, name, args, &RXX), + "ryy" => send(m, name, args, &RYY), + "rzz" => send(m, name, args, &RZZ), + "u3" => send(m, name, args, &U3), + "depolarize" => send(m, name, args, &Depolarize), + "depolarize2" => send(m, name, args, &Depolarize2), + "loss" => send(m, name, args, &Loss), + "pauli_error" => send(m, name, args, &PauliError), + "two_qubit_pauli_error" => send(m, name, args, &TwoQubitPauliError), + "correlated_loss" => send(m, name, args, &CorrelatedLoss), + // Push order is q, p0, p1 (probabilities of leaking into pinned |0⟩ / |1⟩). + "leakage" => send(m, name, args, &Leakage), + _ => bail!("unknown command {name:?}; try :load or device N"), + } +} + +fn send(m: &mut PPVM, name: &str, args: &[&str], instruction: &I) -> Result<()> +where + I: CircuitOp, + I::Message: FromTokens, + PPVM: Record, +{ + let message = I::Message::from_tokens(args, m.n_qubits()).map_err(|e| eyre!("{name}: {e}"))?; + m.send(instruction, message) +} + +/// A circuit message built from command-line operand tokens. +trait FromTokens: Sized { + fn from_tokens(args: &[&str], n_qubits: usize) -> Result; +} + +impl FromTokens for PopFromStack<(u64,)> { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q] = exactly(args)?; + Ok(PopFromStack((qubit(q, n)?,))) + } +} + +impl FromTokens for PopFromStack<(u64, u64)> { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q0, q1] = exactly(args)?; + Ok(PopFromStack((qubit(q0, n)?, qubit(q1, n)?))) + } +} + +impl FromTokens for PopFromStack<(u64, f64)> { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q, p] = exactly(args)?; + Ok(PopFromStack((qubit(q, n)?, param(p)?))) + } +} + +impl FromTokens for PopFromStack<(u64, u64, f64)> { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q0, q1, p] = exactly(args)?; + Ok(PopFromStack((qubit(q0, n)?, qubit(q1, n)?, param(p)?))) + } +} + +impl FromTokens for PopFromStack<(u64, f64, f64)> { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q, p0, p1] = exactly(args)?; + Ok(PopFromStack((qubit(q, n)?, param(p0)?, param(p1)?))) + } +} + +impl FromTokens for PopFromStack<(u64, f64, f64, f64)> { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q, p0, p1, p2] = exactly(args)?; + Ok(PopFromStack(( + qubit(q, n)?, + param(p0)?, + param(p1)?, + param(p2)?, + ))) + } +} + +impl FromTokens for PopFromStack<(u64, u64, f64, f64, f64)> { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q0, q1, p0, p1, p2] = exactly(args)?; + Ok(PopFromStack(( + qubit(q0, n)?, + qubit(q1, n)?, + param(p0)?, + param(p1)?, + param(p2)?, + ))) + } +} + +impl FromTokens for TwoQubitPauliErrorMsg { + fn from_tokens(args: &[&str], n: usize) -> Result { + let [q0, q1, ps @ ..] = exactly::<17>(args)?; + let mut probs = [0.0; 15]; + for (p, tok) in probs.iter_mut().zip(ps) { + *p = param(tok)?; + } + Ok(TwoQubitPauliErrorMsg { + q0: qubit(q0, n)? as usize, + q1: qubit(q1, n)? as usize, + ps: probs, + }) + } +} + +fn exactly<'a, const N: usize>(args: &[&'a str]) -> Result<[&'a str; N]> { + <[&str; N]>::try_from(args).map_err(|_| eyre!("expected {N} operands, got {}", args.len())) +} + +fn qubit(tok: &str, n_qubits: usize) -> Result { + let q = tok + .parse::() + .wrap_err_with(|| format!("invalid qubit index {tok:?}"))?; + if q >= n_qubits { + bail!("qubit {q} out of range for {n_qubits}-qubit device"); + } + Ok(q as u64) +} + +fn param(tok: &str) -> Result { + tok.parse::() + .wrap_err_with(|| format!("invalid parameter {tok:?}")) +} + #[cfg(test)] mod tests { use super::*; + use ppvm_vihaco::composite::PPVMDeviceInfo; + use ppvm_vihaco::measurements::MeasurementOutcome::{self, One, Zero}; - #[test] - fn empty_line_is_step() { - assert_eq!(parse_command(" ").unwrap(), Command::Step); + fn device(n_qubits: usize) -> PPVM { + let info = PPVMDeviceInfo { + n_qubits, + ..Default::default() + }; + PPVM::new(&info, Some(0)).unwrap() } - #[test] - fn device_parses_count() { - assert_eq!(parse_command("device 3").unwrap(), Command::Device(3)); + /// Run gate lines in order, then return every recorded outcome. + fn run(n_qubits: usize, lines: &[&str]) -> Result> { + let mut m = device(n_qubits); + for line in lines { + let mut toks = line.split_whitespace(); + let name = toks.next().unwrap(); + send_gate(&mut m, name, &toks.collect::>())?; + } + Ok(m.measurement_record().iter().flatten().copied().collect()) } #[test] - fn single_qubit_gate() { - assert_eq!( - parse_command("h 0").unwrap(), - Command::Gate { - inst: CircuitInstruction::H, - qubits: vec![0], - params: vec![], - } - ); + fn empty_line_is_step() { + assert_eq!(parse_command(" ").unwrap(), Command::Step); } #[test] - fn two_qubit_gate_keeps_operand_order() { - assert_eq!( - parse_command("cnot 0 1").unwrap(), - Command::Gate { - inst: CircuitInstruction::CNOT, - qubits: vec![0, 1], - params: vec![], - } - ); + fn device_parses_count() { + assert_eq!(parse_command("device 3").unwrap(), Command::Device(3)); } #[test] - fn rotation_parses_float_param() { + fn gate_keeps_operand_order() { assert_eq!( parse_command("rx 0 0.5").unwrap(), Command::Gate { - inst: CircuitInstruction::RX, - qubits: vec![0], - params: vec![0.5], + name: "rx".to_string(), + args: vec!["0".to_string(), "0.5".to_string()], } ); } - #[test] - fn leakage_parses_qubit_and_two_probs() { - assert_eq!( - parse_command("leakage 0 0.0 1.0").unwrap(), - Command::Gate { - inst: CircuitInstruction::Leakage, - qubits: vec![0], - params: vec![0.0, 1.0], - } - ); - assert!(parse_command("leakage 0 1.0").is_err()); - assert!(parse_command("leakage 0").is_err()); - } - #[test] fn meta_commands() { assert_eq!(parse_command(":q").unwrap(), Command::Quit); @@ -239,17 +291,6 @@ mod tests { ); } - #[test] - fn unknown_gate_errors() { - assert!(parse_command("bogus 0").is_err()); - } - - #[test] - fn wrong_arity_errors() { - assert!(parse_command("x").is_err()); - assert!(parse_command("cnot 0").is_err()); - } - #[test] fn load_rejects_trailing_tokens() { assert!(parse_command(":load a.sst").is_ok()); @@ -261,4 +302,79 @@ mod tests { assert!(parse_command("device 2").is_ok()); assert!(parse_command("device 2 extra").is_err()); } + + #[test] + fn x_flips_and_state_persists_across_commands() -> Result<()> { + assert_eq!(run(1, &["measure 0"])?, [Zero]); + assert_eq!(run(1, &["x 0", "measure 0"])?, [One]); + assert_eq!(run(1, &["x 0", "x 0", "measure 0"])?, [Zero]); + Ok(()) + } + + #[test] + fn cnot_control_is_first_operand() -> Result<()> { + assert_eq!( + run(2, &["x 0", "cnot 0 1", "measure 0", "measure 1"])?, + [One, One] + ); + assert_eq!( + run(2, &["x 1", "cnot 0 1", "measure 0", "measure 1"])?, + [Zero, One] + ); + Ok(()) + } + + #[test] + fn adjoint_spellings_invert_their_gate() -> Result<()> { + // In the X basis, S·S† = 1 and T·T† = 1; the H sandwich makes a missing + // or wrong inverse show up as a |1⟩ outcome. + for (g, g_adj) in [("s", "s_adj"), ("t", "t_adj")] { + let lines = [ + "h 0", + &format!("{g} 0"), + &format!("{g_adj} 0"), + "h 0", + "measure 0", + ]; + assert_eq!(run(1, &lines)?, [Zero], "{g} / {g_adj}"); + } + for (g, g_adj) in [("sqrt_x", "sqrt_x_adj"), ("sqrt_y", "sqrt_y_adj")] { + let lines = [ + &format!("{g} 0") as &str, + &format!("{g_adj} 0"), + "measure 0", + ]; + assert_eq!(run(1, &lines)?, [Zero], "{g} / {g_adj}"); + } + // √X · √X = X. + assert_eq!(run(1, &["sqrt_x 0", "sqrt_x 0", "measure 0"])?, [One]); + Ok(()) + } + + #[test] + fn rx_pi_flips() -> Result<()> { + let pi = std::f64::consts::PI.to_string(); + assert_eq!(run(1, &[&format!("rx 0 {pi}"), "measure 0"])?, [One]); + Ok(()) + } + + #[test] + fn bad_operands_error_and_leave_state_untouched() { + let mut m = device(2); + for (name, args) in [ + ("x", &[][..]), + ("cnot", &["0"][..]), + ("rx", &["0"][..]), + ("x", &["2"][..]), + ("x", &["q"][..]), + ("rx", &["0", "half"][..]), + ("bogus", &["0"][..]), + ] { + assert!(send_gate(&mut m, name, args).is_err(), "{name} {args:?}"); + } + send_gate(&mut m, "measure", &["0"]).unwrap(); + send_gate(&mut m, "measure", &["1"]).unwrap(); + let outcomes: Vec<_> = m.measurement_record().iter().flatten().copied().collect(); + assert_eq!(outcomes, [Zero, Zero]); + } } diff --git a/crates/ppvm-vihaco/src/component/executors.rs b/crates/ppvm-vihaco/src/component/executors.rs index 2a51b31a6..711a4c5a9 100644 --- a/crates/ppvm-vihaco/src/component/executors.rs +++ b/crates/ppvm-vihaco/src/component/executors.rs @@ -42,6 +42,7 @@ impl Execute for CircuitExecutor { #[instructions { circuit }] pub struct Circuit { inner: CircuitKind, + n_qubits: usize, } impl Execute for Circuit { @@ -103,12 +104,18 @@ impl Circuit { } } + /// Number of qubits this circuit was built for. + pub fn n_qubits(&self) -> usize { + self.n_qubits + } + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and /// `coefficient_threshold` from `info`; no observable required. pub fn tableau(info: &PPVMDeviceInfo) -> Result { let tab = TableauBuckets::new(info.n_qubits, info.coefficient_threshold)?; Ok(Self { inner: CircuitKind::Tableau(tab), + n_qubits: info.n_qubits, }) } @@ -118,6 +125,7 @@ impl Circuit { let tab = TableauBuckets::new_with_seed(info.n_qubits, info.coefficient_threshold, seed)?; Ok(Self { inner: CircuitKind::Tableau(tab), + n_qubits: info.n_qubits, }) } @@ -129,6 +137,7 @@ impl Circuit { let psum = PauliSumBuckets::new(info, terms)?; Ok(Self { inner: CircuitKind::PauliSum(psum), + n_qubits: info.n_qubits, }) } @@ -138,6 +147,7 @@ impl Circuit { let psum = LossyPauliSumBuckets::new(info, terms)?; Ok(Self { inner: CircuitKind::LossyPauliSum(psum), + n_qubits: info.n_qubits, }) } } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 5c44ae426..7205450fb 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -use crate::component::Circuit; +use crate::component::{Circuit, CircuitOp}; use crate::measurements::{ CircuitOutcomeEffect, MeasurementObserver, MeasurementResult, TraceObserver, }; @@ -131,24 +131,72 @@ where impl vihaco::Apply> for PPVM { fn apply(&mut self, effects: Effects) { for effect in effects { - match effect { + // Programs branch on outcomes: push each as u32 (0/1/2, 2 = Lost), + // and can consume trace values the same way. + match &effect { CircuitOutcomeEffect::Measurement(m) => { - // Programs branch on outcomes: push each as u32 (0/1/2, 2 = Lost). for &outcome in &m.measurement_results { self.execution.apply(PushToStack((outcome as u32,))); } - self.measurement_record.record.push(m.measurement_results) } CircuitOutcomeEffect::Trace(t) => { - // Programs can consume the expectation value, as with outcomes. self.execution.apply(PushToStack((t.value,))); - self.trace_record.record.push(t.value); } } + self.record_outcome(effect); } } } +/// Record a circuit effect without pushing it onto the operand stack. +pub trait Record { + fn record(&mut self, effect: E); +} + +impl Record<()> for PPVM { + fn record(&mut self, _effect: ()) {} +} + +impl Record> for PPVM { + fn record(&mut self, effects: Effects) { + for effect in effects { + self.record_outcome(effect); + } + } +} + +impl PPVM { + fn record_outcome(&mut self, effect: CircuitOutcomeEffect) { + match effect { + CircuitOutcomeEffect::Measurement(m) => { + self.measurement_record.record.push(m.measurement_results) + } + CircuitOutcomeEffect::Trace(t) => self.trace_record.record.push(t.value), + } + } + + /// Execute one circuit instruction on a ready-made message, outside any + /// running program. + /// + /// The operand stack is not touched: outcomes land in the measurement and + /// trace records only. Qubit indices are not range-checked here; callers + /// building messages by hand should check them against `n_qubits`. + pub fn send(&mut self, instruction: &I, message: I::Message) -> eyre::Result<()> + where + I: CircuitOp, + Self: Record, + { + let effect = self.circuit.execute(instruction, message)?; + self.record(effect); + Ok(()) + } + + /// Number of qubits on the circuit device. + pub fn n_qubits(&self) -> usize { + self.circuit.n_qubits() + } +} + impl vihaco::Apply<()> for PPVM { fn apply(&mut self, _effect: ()) {} } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 42d7c08ee..1a6ca4231 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -11,9 +11,8 @@ pub mod measurements; pub mod observable; pub mod shots; -/// Re-exported so consumers (e.g. the CLI REPL) can name gates for -/// [`composite::PPVM::apply_circuit_instruction`] without depending on the ISA -/// crate directly. +/// Re-exported so consumers (e.g. the TUI) can name gates for +/// [`composite::PPVM::send`] without depending on the ISA crate directly. pub use vihaco_circuit_isa::circuit; pub use composite::PPVMModule; From b4e318840cd8d8a439fbd6849da29c9ca8958a10 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 10 Nov 2026 10:41:21 +0100 Subject: [PATCH 33/36] Fix ppvm-tui --- Cargo.lock | 48 +++--- Cargo.toml | 15 ++ crates/ppvm-tui/src/app.rs | 151 +++++++++++------- crates/ppvm-tui/src/command.rs | 2 +- crates/ppvm-tui/src/widgets.rs | 3 +- crates/ppvm-vihaco/src/component/buckets.rs | 41 +++++ crates/ppvm-vihaco/src/component/executors.rs | 5 + crates/ppvm-vihaco/src/composite.rs | 30 ++++ 8 files changed, 201 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb6b89a9f..fe0e211b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,7 +746,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.0", + "unicode-width 0.1.14", ] [[package]] @@ -761,7 +761,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1208,7 +1208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2903,7 +2903,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3238,7 +3238,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3453,8 +3453,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vihaco" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "912af9259a5fb3dba120fe8083388f3792b35bb37008f98a25eef1c3597b8ec3" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -3477,8 +3476,7 @@ dependencies = [ [[package]] name = "vihaco-abi" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a6fe13d301df45ac93545cce68166c6afc408f1b1e465e1ceb2d5be39f82930" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -3491,8 +3489,7 @@ dependencies = [ [[package]] name = "vihaco-abi-derive" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa2b9efe3e360c8ec0cf9b9414feee479131e130e0041385fdc740d038e3e43" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3503,8 +3500,7 @@ dependencies = [ [[package]] name = "vihaco-bytecode" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc55ca2deeadfb201ca4764e13e4beca158dfac3c3a8e5a77199d33884ad713c" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -3526,8 +3522,7 @@ dependencies = [ [[package]] name = "vihaco-cpu" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50058ee0dea40ca48c685704b202cc32f0c9674a5989750fdb87ef38f995f1d" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "chumsky 0.10.1", "codespan", @@ -3543,8 +3538,7 @@ dependencies = [ [[package]] name = "vihaco-module" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93cb72f8967f231c48c04396532ed2ac98284db3ee5be0c7867c4e3d6d2a2636" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "chumsky 0.10.1", "colored", @@ -3558,8 +3552,7 @@ dependencies = [ [[package]] name = "vihaco-parser" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c7c6ff12e013b826629b49935f5ed1b7995528984bb8f1e77ccc78f370ac33" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -3569,8 +3562,7 @@ dependencies = [ [[package]] name = "vihaco-parser-derive" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85e1d714a9af3031cc85cb2b4bd7e2c6d0ad2dee20c78e432182877ec2b16dc" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "chumsky 0.10.1", "eyre", @@ -3583,8 +3575,7 @@ dependencies = [ [[package]] name = "vihaco-runtime" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb5bd80466633b93da51327cf3ef6cb66e578738f9b815b067e54b196d60544" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "chumsky 0.10.1", "eyre", @@ -3600,8 +3591,7 @@ dependencies = [ [[package]] name = "vihaco-runtime-derive" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a7d0045bf51b27ffc426d0d2627b9cc00ebab0a95363c19c3007d850b257af" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "convert_case", "proc-macro-crate", @@ -3613,8 +3603,7 @@ dependencies = [ [[package]] name = "vihaco-stdlib" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52426a39b5d6bf07fa21ad1297250fc1bd7973d482b08782f9689f4acc295881" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "chumsky 0.10.1", "eyre", @@ -3630,8 +3619,7 @@ dependencies = [ [[package]] name = "vihaco-syntax" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7065542523d4473d92546c6f1087142945e7bfd1ab3b7f2a163bac968ebf87" +source = "git+https://github.com/QuEraComputing/vihaco?branch=feat%2Fprogram-breakpoint-span#ce4e18d31c6cd67785de20ba08abc3a6cae640de" dependencies = [ "chumsky 0.10.1", "eyre", @@ -3834,7 +3822,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 783125d84..826ccbc10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,3 +49,18 @@ debug = "line-tables-only" [[example]] name = "symbolic" + +# TODO: drop once vihaco#170 (program::breakpoint) is released. +[patch.crates-io] +vihaco = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-abi = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-abi-derive = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-bytecode = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-cpu = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-module = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-parser = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-parser-derive = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-runtime = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-runtime-derive = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-stdlib = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } +vihaco-syntax = { git = "https://github.com/QuEraComputing/vihaco", branch = "feat/program-breakpoint-span" } diff --git a/crates/ppvm-tui/src/app.rs b/crates/ppvm-tui/src/app.rs index 5fbfa9be8..dfe15b628 100644 --- a/crates/ppvm-tui/src/app.rs +++ b/crates/ppvm-tui/src/app.rs @@ -8,7 +8,7 @@ //! touches a terminal or runs a loop. use eyre::{Result, eyre}; -use ppvm_vihaco::composite::{PPVM, StepOutcome}; +use ppvm_vihaco::composite::{PPVM, PPVMDeviceInfo}; use ppvm_vihaco::measurements::MeasurementResult; use ppvm_vihaco::{PPVMModule, compile_program, load_module_file}; use ratatui::Frame; @@ -36,7 +36,7 @@ pub struct AppState { /// True while a program is loaded (Program panel) vs a REPL session (Log). has_program: bool, /// True while the debugger is stopped and waiting for input (at start, after - /// a step, or at a cpu::cpu.breakpoint). + /// a step, or at a breakpoint). paused: bool, /// True once the loaded program has run to Return/Halt. finished: bool, @@ -112,7 +112,7 @@ impl AppState { } fn new_device(&mut self, n: usize) -> Result<()> { - self.machine = Some(PPVM::with_qubits(n)?); + self.machine = Some(fresh_device(n)?); // Forget any previously loaded program so `:reset` resets this device // rather than resurrecting the old program. self.module = None; @@ -155,17 +155,22 @@ impl AppState { /// Core loader: rebuild the machine from `module` and pause at pc 0. /// Returns `false` if load/init fails (error is written to the status line). fn load_module(&mut self, module: PPVMModule) -> bool { - let mut m = PPVM::default(); - // A fresh machine + load + init gives clean tableau/record state; these - // only fail on malformed modules, which `compile_program` already - // rejects, so surface as a status rather than unwinding the UI. - if let Err(e) = m.load(&module).and_then(|()| m.init()) { - self.set_status(format!("error: {e}")); - return false; - } + // A fresh machine gives clean state; this only fails on malformed + // modules, so surface it as a status rather than unwinding the UI. + let m = PPVM::new(&module.extra, None).and_then(|mut m| { + m.start(&module)?; + Ok(m) + }); + let m = match m { + Ok(m) => m, + Err(e) => { + self.set_status(format!("error: {e}")); + return false; + } + }; self.program.clear(); for (i, inst) in module.code.iter().enumerate() { - self.program.push(format!("{i:04}: {inst}")); + self.program.push(format!("{i:04}: {inst:?}")); } self.machine = Some(m); self.module = Some(module); @@ -177,7 +182,7 @@ impl AppState { } fn refresh_cursor(&mut self) { - let pc = self.machine.as_ref().map(|m| m.current_pc() as usize); + let pc = self.machine.as_ref().map(|m| m.pc() as usize); if let Some(pc) = pc { self.program.set_cursor(Some(pc)); } @@ -194,10 +199,8 @@ impl AppState { self.set_status("program finished — :reset to run again"); return Ok(()); } - let outcome = self.machine.as_mut().unwrap().step_once()?; - self.apply_outcome(outcome); - self.refresh_cursor(); - Ok(()) + // TODO: vihaco has no single-instruction step API yet (only `resume`). + Err(eyre!("single-step is not supported yet — use :continue")) } fn cont(&mut self) -> Result<()> { @@ -205,50 +208,33 @@ impl AppState { self.set_status("nothing to continue — load a program with :load"); return Ok(()); } - while !self.finished { - let outcome = self.machine.as_mut().unwrap().step_once()?; - match outcome { - StepOutcome::Continue => {} - StepOutcome::Breakpoint => { - self.apply_outcome(outcome); - self.refresh_cursor(); - return Ok(()); - } - StepOutcome::Return | StepOutcome::Halt => { - self.apply_outcome(outcome); - break; - } - } + if self.finished { + self.set_status("program finished — :reset to run again"); + return Ok(()); + } + let (Some(m), Some(module)) = (self.machine.as_mut(), self.module.as_ref()) else { + return Ok(()); + }; + m.resume(module)?; + if m.is_paused() { + self.paused = true; + self.set_status("-- breakpoint hit --"); + } else { + self.paused = false; + self.finished = true; + self.set_status("program finished"); } self.refresh_cursor(); Ok(()) } - /// Fold a single step outcome into the app's paused/finished/status state. - fn apply_outcome(&mut self, outcome: StepOutcome) { - match outcome { - StepOutcome::Continue => { - self.set_status(""); - } - StepOutcome::Breakpoint => { - self.paused = true; - self.set_status("-- cpu::cpu.breakpoint hit --"); - } - StepOutcome::Return | StepOutcome::Halt => { - self.paused = false; - self.finished = true; - self.set_status("program finished"); - } - } - } - fn reset(&mut self) -> Result<()> { if let Some(module) = self.module.clone() { if self.load_module(module) { self.set_status("reset"); } } else if self.n_qubits > 0 { - self.machine = Some(PPVM::with_qubits(self.n_qubits)?); + self.machine = Some(fresh_device(self.n_qubits)?); self.set_status("reset device"); } else { self.set_status("nothing to reset"); @@ -451,7 +437,7 @@ Meta / debug device N create a fresh N-qubit tableau device :load load a .sst / .ssb program (paused at start) Enter (empty) :s step one instruction - :continue :c run to the next cpu::cpu.breakpoint or the end + :continue :c run to the next breakpoint or the end :reset restart the loaded program / device :help :h toggle this help :quit :q (Ctrl-C) leave @@ -470,6 +456,15 @@ Gates (q = qubit index; angles / probabilities are floats) Line editing: ←/→ move · Home/End · Backspace/Del · ↑/↓ history"; +/// A fresh `n_qubits`-qubit tableau device with no program. +fn fresh_device(n_qubits: usize) -> Result { + let info = PPVMDeviceInfo { + n_qubits, + ..Default::default() + }; + PPVM::new(&info, None) +} + /// Render a measurement record as flat bits: `Zero`→`0`, `One`→`1`, `Lost`→`2` /// (the outcome's own enum value), events joined by spaces. fn format_record(record: &[MeasurementResult]) -> String { @@ -486,7 +481,7 @@ fn format_record(record: &[MeasurementResult]) -> String { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; fn key(code: KeyCode) -> KeyEvent { @@ -597,9 +592,42 @@ mod tests { assert!(app.should_exit); } - /// A 1-qubit program with a cpu::cpu.breakpoint before measuring q0 (|0> -> 0). - const BP_PROGRAM: &str = "device circuit.n_qubits 1;\n\ - fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; + /// A 1-qubit program with a breakpoint before measuring q0 (|0> -> 0). + pub(crate) const BP_PROGRAM: &str = r#"sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "bp.sst"], + functions: [ + { + name: 0, + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 4, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + execution::program::breakpoint + execution::program::const_u64 0 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). +"#; #[test] fn load_source_starts_paused_with_a_listing() { @@ -619,7 +647,7 @@ mod tests { app.load_source(BP_PROGRAM).unwrap(); app.dispatch(":c"); assert!( - app.status().contains("cpu::cpu.breakpoint"), + app.status().contains("breakpoint"), "status: {}", app.status() ); @@ -637,7 +665,7 @@ mod tests { fn finishing_clears_paused() { let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); - app.dispatch(":c"); // pause at cpu::cpu.breakpoint + app.dispatch(":c"); // pause at breakpoint assert!(app.paused()); app.dispatch(":c"); // run to Return assert!(!app.paused(), "paused must clear once the program finishes"); @@ -647,7 +675,7 @@ mod tests { fn finished_hint_does_not_suggest_stepping() { let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); - app.dispatch(":c"); // pause at cpu::cpu.breakpoint + app.dispatch(":c"); // pause at breakpoint app.dispatch(":c"); // run to Return assert!( !app.hint().contains("step") && !app.hint().contains("continue"), @@ -657,6 +685,7 @@ mod tests { } #[test] + #[ignore = "TODO: needs a single-step API in vihaco"] fn empty_line_steps_and_advances_cursor() { let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); @@ -681,11 +710,11 @@ mod tests { #[test] fn inject_gate_at_breakpoint_then_resume() { - // At the cpu::cpu.breakpoint, inject X on q0; resuming, the program measures |1>. + // At the breakpoint, inject X on q0; resuming, the program measures |1>. let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); - app.dispatch(":c"); // run to the cpu::cpu.breakpoint - assert!(app.status().contains("cpu::cpu.breakpoint")); + app.dispatch(":c"); // run to the breakpoint + assert!(app.status().contains("breakpoint")); app.dispatch("x 0"); // inject while paused app.dispatch(":c"); // resume; program measures q0 assert!( diff --git a/crates/ppvm-tui/src/command.rs b/crates/ppvm-tui/src/command.rs index 956c621d2..56e3fc95d 100644 --- a/crates/ppvm-tui/src/command.rs +++ b/crates/ppvm-tui/src/command.rs @@ -20,7 +20,7 @@ pub enum Command { Gate { name: String, args: Vec }, /// Advance one instruction (also the meaning of an empty line). Step, - /// Run to the next cpu::cpu.breakpoint or program end. + /// Run to the next breakpoint or program end. Continue, /// Reset the loaded program / device to its initial state. Reset, diff --git a/crates/ppvm-tui/src/widgets.rs b/crates/ppvm-tui/src/widgets.rs index 5a18a1d03..2933c07c5 100644 --- a/crates/ppvm-tui/src/widgets.rs +++ b/crates/ppvm-tui/src/widgets.rs @@ -92,8 +92,7 @@ mod tests { use ratatui::Terminal; use ratatui::backend::TestBackend; - const BP_PROGRAM: &str = "device circuit.n_qubits 1;\n\ - fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; + use crate::app::tests::BP_PROGRAM; #[test] fn renders_all_panels_without_panic() { diff --git a/crates/ppvm-vihaco/src/component/buckets.rs b/crates/ppvm-vihaco/src/component/buckets.rs index 9948bb005..216bedf14 100644 --- a/crates/ppvm-vihaco/src/component/buckets.rs +++ b/crates/ppvm-vihaco/src/component/buckets.rs @@ -24,6 +24,17 @@ pub(crate) enum TableauBuckets { } impl TableauBuckets { + pub(crate) fn state_string(&self) -> String { + match self { + Self::Bits64(c) => c.backend.to_string(), + Self::Bits128(c) => c.backend.to_string(), + Self::Bits256(c) => c.backend.to_string(), + Self::Bits512(c) => c.backend.to_string(), + Self::Bits1024(c) => c.backend.to_string(), + Self::Bits2048(c) => c.backend.to_string(), + } + } + pub(crate) fn apply( &mut self, op: &I, @@ -115,6 +126,17 @@ pub(crate) enum PauliSumBuckets { } impl PauliSumBuckets { + pub(crate) fn state_string(&self) -> String { + match self { + Self::Bits64(c) => c.backend.state.to_string(), + Self::Bits128(c) => c.backend.state.to_string(), + Self::Bits256(c) => c.backend.state.to_string(), + Self::Bits512(c) => c.backend.state.to_string(), + Self::Bits1024(c) => c.backend.state.to_string(), + Self::Bits2048(c) => c.backend.state.to_string(), + } + } + pub(crate) fn apply( &mut self, op: &I, @@ -193,6 +215,17 @@ pub(crate) enum LossyPauliSumBuckets { } impl LossyPauliSumBuckets { + pub(crate) fn state_string(&self) -> String { + match self { + Self::Bits64(c) => c.backend.state.to_string(), + Self::Bits128(c) => c.backend.state.to_string(), + Self::Bits256(c) => c.backend.state.to_string(), + Self::Bits512(c) => c.backend.state.to_string(), + Self::Bits1024(c) => c.backend.state.to_string(), + Self::Bits2048(c) => c.backend.state.to_string(), + } + } + pub(crate) fn apply( &mut self, op: &I, @@ -265,6 +298,14 @@ pub(crate) enum CircuitKind { } impl CircuitKind { + pub(crate) fn state_string(&self) -> String { + match self { + Self::Tableau(b) => b.state_string(), + Self::PauliSum(b) => b.state_string(), + Self::LossyPauliSum(b) => b.state_string(), + } + } + pub(crate) fn reset(&mut self) { match self { Self::Tableau(b) => b.reset(), diff --git a/crates/ppvm-vihaco/src/component/executors.rs b/crates/ppvm-vihaco/src/component/executors.rs index 711a4c5a9..1e6331eb3 100644 --- a/crates/ppvm-vihaco/src/component/executors.rs +++ b/crates/ppvm-vihaco/src/component/executors.rs @@ -109,6 +109,11 @@ impl Circuit { self.n_qubits } + /// Human-readable rendering of the backend state (tableau or Pauli sum). + pub fn state_string(&self) -> String { + self.inner.state_string() + } + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and /// `coefficient_threshold` from `info`; no observable required. pub fn tableau(info: &PPVMDeviceInfo) -> Result { diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 7205450fb..243d31f76 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -195,6 +195,36 @@ impl PPVM { pub fn n_qubits(&self) -> usize { self.circuit.n_qubits() } + + /// Human-readable rendering of the circuit state. + pub fn state_string(&self) -> String { + self.circuit.state_string() + } + + /// Enter `module`'s main without executing anything; `resume` runs it. + pub fn start(&mut self, module: &Program) -> eyre::Result<()> { + self.execution.start(module, &[]) + } + + /// Run the started program until it halts, returns, or hits a breakpoint. + pub fn resume(&mut self, module: &Program) -> eyre::Result<()> { + vihaco::resume(module, self) + } + + /// Index of the next instruction to execute. + pub fn pc(&self) -> u32 { + self.execution.pc() + } + + /// True while a breakpoint has suspended the running program. + pub fn is_paused(&self) -> bool { + self.execution.is_paused() + } + + /// True once the program has finished (or before one was started). + pub fn is_halted(&self) -> bool { + self.execution.is_halted() + } } impl vihaco::Apply<()> for PPVM { From d046b9c9d58a9d6e39b446a758bf8fe25df2c6b7 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 10 Nov 2026 11:15:22 +0100 Subject: [PATCH 34/36] Fix ppmv-cli --- crates/ppvm-cli/src/commands.rs | 108 +++++++++--------- crates/ppvm-cli/src/main.rs | 12 +- crates/ppvm-tui/src/app.rs | 39 +------ crates/ppvm-tui/src/command.rs | 2 +- crates/ppvm-vihaco/src/lib.rs | 2 +- crates/ppvm-vihaco/src/loader.rs | 19 +-- .../ppvm-vihaco/tests/breakpoint_measure.sst | 35 ++++++ crates/ppvm-vihaco/tests/measure_zero.sst | 34 ++++++ 8 files changed, 148 insertions(+), 103 deletions(-) create mode 100644 crates/ppvm-vihaco/tests/breakpoint_measure.sst create mode 100644 crates/ppvm-vihaco/tests/measure_zero.sst diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index fd17a2705..1b2996943 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use eyre::{Result, WrapErr}; -use ppvm_vihaco::composite::{PPVM, StepOutcome}; +use ppvm_vihaco::composite::{PPVM, PPVMModule}; use ppvm_vihaco::measurements::MeasurementResult; use ppvm_vihaco::shots::ShotRecord; use std::io::{BufRead, Write}; @@ -120,8 +120,9 @@ pub fn parse(file: &str, format: Format) -> Result<()> { } Format::Pretty => { println!("Module:"); - println!(" Headers: {}", parsed.header.len()); - for (i, header) in parsed.header.iter().enumerate() { + let headers: Vec<_> = parsed.header.into_iter().collect(); + println!(" Headers: {}", headers.len()); + for (i, header) in headers.iter().enumerate() { println!(" [{}] {:?}", i, header); } println!(" Functions: {}", parsed.functions.len()); @@ -170,9 +171,8 @@ enum DebugCommand { Quit, } -/// Step through a program interactively, pausing at `cpu::cpu.breakpoint` instructions. -/// With `break_at_start`, also pauses before the first instruction so any -/// program can be stepped from the beginning. +/// Run a program interactively, pausing at breakpoints. With `break_at_start`, +/// also pauses before the first instruction. pub fn debug(file: &str, break_at_start: bool) -> Result<()> { let stdin = std::io::stdin(); let mut input = stdin.lock(); @@ -187,45 +187,42 @@ fn debug_loop( input: &mut impl BufRead, output: &mut impl Write, ) -> Result<()> { - let mut machine = PPVM::default(); - machine - .load_file(file) - .wrap_err_with(|| format!("failed to load {file}"))?; - machine.init()?; + let module = + ppvm_vihaco::load_module_file(file).wrap_err_with(|| format!("failed to load {file}"))?; + let mut machine = PPVM::new(&module.extra, None)?; + machine.start(&module)?; let mut paused = break_at_start; let mut ever_paused = paused; loop { - // Safety net: stop if execution has run off the end of the code. - if machine.current_instruction().is_none() { - writeln!(output, "Program counter past end of code.")?; - break; - } - if paused { - print_location(&machine, output)?; + print_location(&machine, &module, output)?; match read_command(input, output)? { DebugCommand::Quit => { writeln!(output, "Quit.")?; return Ok(()); } - DebugCommand::Continue => paused = false, - DebugCommand::Step => {} + DebugCommand::Continue => {} + DebugCommand::Step => { + // TODO: vihaco has no single-instruction step API yet (only `resume`). + writeln!( + output, + "single-step is not supported yet — use c to continue" + )?; + continue; + } } } - match machine.step_once()? { - StepOutcome::Continue => {} - StepOutcome::Breakpoint => { - paused = true; - ever_paused = true; - writeln!(output, "-- cpu::cpu.breakpoint hit --")?; - } - StepOutcome::Return | StepOutcome::Halt => { - writeln!(output, "Program finished.")?; - break; - } + machine.resume(&module)?; + if machine.is_paused() { + paused = true; + ever_paused = true; + writeln!(output, "-- breakpoint hit --")?; + } else { + writeln!(output, "Program finished.")?; + break; } } @@ -241,17 +238,17 @@ fn debug_loop( if !ever_paused { writeln!( output, - "(no cpu::cpu.breakpoint was hit; pass --break-at-start to step from the beginning)" + "(no breakpoint was hit; pass --break-at-start to pause before the first instruction)" )?; } Ok(()) } /// Print the program counter, the next instruction, and measurements so far. -fn print_location(machine: &PPVM, output: &mut impl Write) -> Result<()> { - let pc = machine.current_pc(); - match machine.current_instruction() { - Some(inst) => writeln!(output, "pc={pc} next: {inst}")?, +fn print_location(machine: &PPVM, module: &PPVMModule, output: &mut impl Write) -> Result<()> { + let pc = machine.pc(); + match module.code.get(pc as usize) { + Some(inst) => writeln!(output, "pc={pc} next: {inst:?}")?, None => writeln!(output, "pc={pc} (end of code)")?, } writeln!( @@ -292,8 +289,8 @@ mod tests { use ppvm_vihaco::measurements::MeasurementOutcome; use std::fs; - /// Minimal program that compiles and measures q0 in |0> (deterministic). - const PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; + /// Measures q0 in |0> (deterministic). + const PROGRAM: &str = include_str!("../../ppvm-vihaco/tests/measure_zero.sst"); fn row(outcomes: &[MeasurementOutcome]) -> MeasurementResult { outcomes.iter().copied().collect() @@ -406,12 +403,8 @@ mod tests { #[test] fn run_outputs_trace_record_for_paulisum_backend() { - // Single-qubit PauliSum with observable Z, no gates, trace `Z?*` → 1.0. - // The .sst-driven path should surface the trace value in `run`'s output. - const TRACE_PROGRAM: &str = "device circuit.n_qubits 1;\n\ - device circuit.backend paulisum;\n\ - device circuit.observable Z;\n\ - fn @main() { cpu::cpu.const str, \"Z?*\"\n circuit::circuit.trace\n cpu::cpu.ret 0 }\n"; + // PauliSum Bell circuit tracing ⟨ZZ⟩ = 1.0; `run` should surface the trace value. + const TRACE_PROGRAM: &str = include_str!("../../ppvm-vihaco/tests/paulisum_bell_trace.sst"); let src = temp_file("ppvm_cli_run_trace.sst", TRACE_PROGRAM); let out = std::env::temp_dir().join("ppvm_cli_run_trace.txt"); let _ = fs::remove_file(&out); @@ -514,8 +507,8 @@ mod tests { // ─── debug ───────────────────────────────────────────────────────── - /// Program with a `cpu::cpu.breakpoint` before measuring q0 in |0> (deterministic). - const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; + /// A breakpoint, then measuring q0 in |0> (deterministic). + const BREAKPOINT_PROGRAM: &str = include_str!("../../ppvm-vihaco/tests/breakpoint_measure.sst"); /// Drive `debug_loop` with scripted input, returning the captured output. fn run_debug(program: &str, name: &str, break_at_start: bool, script: &str) -> String { @@ -528,6 +521,7 @@ mod tests { } #[test] + #[ignore = "TODO: needs a single-step API in vihaco"] fn debug_break_at_start_steps_through_to_finish() { // PROGRAM is cpu::cpu.const u64, 0 / circuit::circuit.measure / ret = 3 steps. let out = run_debug(PROGRAM, "ppvm_cli_debug_step.sst", true, "s\ns\ns\n"); @@ -542,6 +536,16 @@ mod tests { ); } + #[test] + fn debug_break_at_start_pauses_before_running() { + let out = run_debug(PROGRAM, "ppvm_cli_debug_start.sst", true, "s\nc\n"); + assert!(out.contains("pc=0"), "should pause at the entry: {out}"); + assert!(out.contains("measurements: \n"), "nothing ran yet: {out}"); + assert!(out.contains("single-step is not supported yet"), "{out}"); + assert!(out.contains("Program finished."), "{out}"); + assert!(out.contains("Measurements: 0"), "{out}"); + } + #[test] fn debug_continue_runs_to_end() { let out = run_debug(PROGRAM, "ppvm_cli_debug_continue.sst", true, "c\n"); @@ -551,12 +555,12 @@ mod tests { #[test] fn debug_honors_authored_breakpoint() { - // Not breaking at start: must run until the `cpu::cpu.breakpoint` pauses it. + // Not breaking at start: must run until the `breakpoint` pauses it. let out = run_debug(BREAKPOINT_PROGRAM, "ppvm_cli_debug_bp.sst", false, "c\n"); - assert!(out.contains("-- cpu::cpu.breakpoint hit --"), "{out}"); + assert!(out.contains("-- breakpoint hit --"), "{out}"); assert!(out.contains("Program finished."), "{out}"); - // A cpu::cpu.breakpoint was hit, so no "use --break-at-start" hint. - assert!(!out.contains("no cpu::cpu.breakpoint was hit"), "{out}"); + // A breakpoint was hit, so no "use --break-at-start" hint. + assert!(!out.contains("no breakpoint was hit"), "{out}"); } #[test] @@ -572,10 +576,10 @@ mod tests { #[test] fn debug_without_breakpoint_prints_hint() { - // No cpu::cpu.breakpoint, no break-at-start, empty input: runs straight through + // No breakpoint, no break-at-start, empty input: runs straight through // and tells the user how to step. let out = run_debug(PROGRAM, "ppvm_cli_debug_hint.sst", false, ""); assert!(out.contains("Program finished."), "{out}"); - assert!(out.contains("no cpu::cpu.breakpoint was hit"), "{out}"); + assert!(out.contains("no breakpoint was hit"), "{out}"); } } diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index 5a453b527..239584b35 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -15,7 +15,7 @@ pub struct Cli { #[arg(short, long, global = true, default_value_t = 1, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..))] threads: usize, - /// A .sst/.ssb file to open in the TUI (when no subcommand is given). + /// A .sst file to open in the TUI (when no subcommand is given). #[arg(value_name = "FILE")] file: Option, @@ -51,9 +51,9 @@ enum Commands { // #[arg(short, long)] // force: bool, // }, - /// Run a .sst or .ssb program + /// Run a .sst program Run { - /// Input file (.sst source or .ssb bytecode) + /// Input .sst file #[arg(value_name = "FILE")] file: String, @@ -78,13 +78,13 @@ enum Commands { format: commands::MeasurementFormat, }, - /// Step through a program interactively, pausing at `cpu::cpu.breakpoint` instructions + /// Run a program interactively, pausing at breakpoints Debug { - /// Input file (.sst source or .ssb bytecode) + /// Input .sst file #[arg(value_name = "FILE")] file: String, - /// Pause before the first instruction so any program can be stepped + /// Pause before the first instruction #[arg(short, long)] break_at_start: bool, }, diff --git a/crates/ppvm-tui/src/app.rs b/crates/ppvm-tui/src/app.rs index dfe15b628..007ce14f2 100644 --- a/crates/ppvm-tui/src/app.rs +++ b/crates/ppvm-tui/src/app.rs @@ -435,7 +435,7 @@ impl AppState { const HELP_TEXT: &str = "\ Meta / debug device N create a fresh N-qubit tableau device - :load load a .sst / .ssb program (paused at start) + :load load a .sst program (paused at start) Enter (empty) :s step one instruction :continue :c run to the next breakpoint or the end :reset restart the loaded program / device @@ -593,41 +593,8 @@ pub(crate) mod tests { } /// A 1-qubit program with a breakpoint before measuring q0 (|0> -> 0). - pub(crate) const BP_PROGRAM: &str = r#"sst v1 -.section(root): - .section(ppvm): - .header(ppvm): - circuit.n_qubits 1; - metadata { - strings: ["main", "bp.sst"], - functions: [ - { - name: 0, - signature: { params: [], ret: [] }, - local_count: 0, - start_address: 0, - end_address: 4, - file: 1, - }, - ], - labels: [], - constants: [], - main_function: 0, - source_symbols: [], - extra: {}, - }; - .header(ppvm). - .text(ppvm): - fn @main() { - execution::program::breakpoint - execution::program::const_u64 0 - circuit::circuit::measure - execution::program::ret 0 - } - .text(ppvm). - .section(ppvm). -.section(root). -"#; + pub(crate) const BP_PROGRAM: &str = + include_str!("../../ppvm-vihaco/tests/breakpoint_measure.sst"); #[test] fn load_source_starts_paused_with_a_listing() { diff --git a/crates/ppvm-tui/src/command.rs b/crates/ppvm-tui/src/command.rs index 56e3fc95d..6c32d09e6 100644 --- a/crates/ppvm-tui/src/command.rs +++ b/crates/ppvm-tui/src/command.rs @@ -24,7 +24,7 @@ pub enum Command { Continue, /// Reset the loaded program / device to its initial state. Reset, - /// Load a `.sst`/`.ssb` file. + /// Load a `.sst` file. Load(String), /// Toggle the help overlay. Help, diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 1a6ca4231..a8cbeaec8 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -16,7 +16,7 @@ pub mod shots; pub use vihaco_circuit_isa::circuit; pub use composite::PPVMModule; -pub use loader::{compile_program, load_module_file}; +pub use loader::{compile_program, load_module_file, parse_program}; // use crate::composite::ppvm_module as ppvm; // use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; diff --git a/crates/ppvm-vihaco/src/loader.rs b/crates/ppvm-vihaco/src/loader.rs index de20011b6..d86842757 100644 --- a/crates/ppvm-vihaco/src/loader.rs +++ b/crates/ppvm-vihaco/src/loader.rs @@ -14,19 +14,24 @@ pub const PPVM_SECTION: &str = "ppvm"; type SourceInstruction = ::Syntax; -/// Parse and lower SST source into a module whose `extra` is the device info. -pub fn compile_program(source: &str) -> eyre::Result { +/// A parsed but not yet lowered PPVM program: header declarations and functions. +pub type ParsedProgram = + ParsedModule>; + +/// Parse SST source into its header declarations and functions, without lowering. +pub fn parse_program(source: &str) -> eyre::Result { let file = SstFile::::from_text(source)?; let section = file .root() .child(PPVM_SECTION) .ok_or_else(|| eyre::eyre!("missing `{PPVM_SECTION}` section"))?; - let ParsedModule { header, functions } = ParsedModule::< - SourceInstruction, - Type, - HeaderDeclarations, - >::parse_section(section)?; + ParsedProgram::parse_section(section) +} + +/// Parse and lower SST source into a module whose `extra` is the device info. +pub fn compile_program(source: &str) -> eyre::Result { + let ParsedModule { header, functions } = parse_program(source)?; let (metadata, info) = collect_header(header)?; let mut context = MetadataLoweringContext::new(&metadata)?; diff --git a/crates/ppvm-vihaco/tests/breakpoint_measure.sst b/crates/ppvm-vihaco/tests/breakpoint_measure.sst new file mode 100644 index 000000000..7875c75b4 --- /dev/null +++ b/crates/ppvm-vihaco/tests/breakpoint_measure.sst @@ -0,0 +1,35 @@ +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "breakpoint_measure.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 4, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Pause, then measure q0 in |0> (0 unless a gate is injected at the pause). + execution::program::breakpoint + execution::program::const_u64 0 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). diff --git a/crates/ppvm-vihaco/tests/measure_zero.sst b/crates/ppvm-vihaco/tests/measure_zero.sst new file mode 100644 index 000000000..0edf78069 --- /dev/null +++ b/crates/ppvm-vihaco/tests/measure_zero.sst @@ -0,0 +1,34 @@ +sst v1 +.section(root): + .section(ppvm): + .header(ppvm): + circuit.n_qubits 1; + metadata { + strings: ["main", "measure_zero.sst"], + functions: [ + { + name: 0, // main + signature: { params: [], ret: [] }, + local_count: 0, + start_address: 0, + end_address: 3, + file: 1, + }, + ], + labels: [], + constants: [], + main_function: 0, // main + source_symbols: [], + extra: {}, + }; + .header(ppvm). + .text(ppvm): + fn @main() { + // Measure q0 in |0>: the outcome is deterministically 0. + execution::program::const_u64 0 + circuit::circuit::measure + execution::program::ret 0 + } + .text(ppvm). + .section(ppvm). +.section(root). From ce896d09773f9a6291d4cd88df7551f18e64030d Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 10 Nov 2026 11:18:58 +0100 Subject: [PATCH 35/36] Some small cleanup --- crates/ppvm-vihaco/src/component/backend.rs | 2 +- crates/ppvm-vihaco/src/component/ops.rs | 20 ----- crates/ppvm-vihaco/src/lib.rs | 95 --------------------- 3 files changed, 1 insertion(+), 116 deletions(-) diff --git a/crates/ppvm-vihaco/src/component/backend.rs b/crates/ppvm-vihaco/src/component/backend.rs index 61532ee65..33f4fdd7f 100644 --- a/crates/ppvm-vihaco/src/component/backend.rs +++ b/crates/ppvm-vihaco/src/component/backend.rs @@ -69,7 +69,7 @@ pub trait CircuitBackend { Err(eyre!("measure is not supported on this backend")) } - fn leakage(&mut self, _addr0: usize, p0: f64, p1: f64) -> eyre::Result<()> { + fn leakage(&mut self, _addr0: usize, _p0: f64, _p1: f64) -> eyre::Result<()> { Err(eyre!("leakage is not supported on this backend")) } diff --git a/crates/ppvm-vihaco/src/component/ops.rs b/crates/ppvm-vihaco/src/component/ops.rs index ec523e18e..849f1194f 100644 --- a/crates/ppvm-vihaco/src/component/ops.rs +++ b/crates/ppvm-vihaco/src/component/ops.rs @@ -12,26 +12,6 @@ use vihaco_circuit_isa::messages::{ QubitBatch, QubitPairBatch, TraceMessage, TwoQubitPauliErrorMsg, }; -macro_rules! batch_for { - ($tab:expr, $method:ident, $addrs:expr) => { - for addr in $addrs { $tab.$method(*addr); } - }; - ($tab:expr, $method:ident, $addrs:expr, $($arg:expr),+) => { - for addr in $addrs { $tab.$method(*addr, $($arg),+); } - }; -} - -/// Two-qubit sibling of [`batch_for!`]: drives a method that takes two qubit -/// addresses (plus optional extra args) over a slice of `(usize, usize)` pairs. -macro_rules! batch_pairs_for { - ($state:expr, $method:ident, $pairs:expr) => { - for &(a, b) in $pairs { $state.$method(a, b); } - }; - ($state:expr, $method:ident, $pairs:expr, $($arg:expr),+) => { - for &(a, b) in $pairs { $state.$method(a, b, $($arg),+); } - }; -} - // CircuitOp trait: define how an instruction executes on a generic backend // This is used to avoid having huge where constraints on the enums later // - without this trait, we'd have to constrain every enum variant to all Execute impls diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index a8cbeaec8..ded7d891f 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -18,102 +18,7 @@ pub use vihaco_circuit_isa::circuit; pub use composite::PPVMModule; pub use loader::{compile_program, load_module_file, parse_program}; -// use crate::composite::ppvm_module as ppvm; -// use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; -// use crate::syntax::{PPVMHeader, PPVMResolver, parse_functions, parse_headers}; - -// /// A fully resolved PPVM module, ready to load into a [`PPVM`]. -// pub type PPVMModule = LocalModule; - -// /// Read a file and produce a loadable module, auto-detecting the format: a -// /// leading PPVM magic is parsed as `.ssb` bytecode, otherwise as `.sst` source. -// /// Mirrors [`PPVM::load_file`] but returns the module so it can be compiled once -// /// and run for many shots. -// pub fn load_module_file(path: &str) -> eyre::Result { -// let bytes = std::fs::read(path)?; -// if bytecode::is_bytecode(&bytes) { -// bytecode::module_from_bytes(&bytes) -// } else { -// compile_program(std::str::from_utf8(&bytes)?) -// } -// } - -// pub fn run_file(path: &str) -> eyre::Result { -// let mut machine = PPVM::default(); -// machine.run_file(path)?; -// Ok(machine) -// } - -// pub fn run_program(program: &str) -> eyre::Result { -// let mut machine = PPVM::default(); -// machine.run_program(program)?; -// Ok(machine) -// } - -// // /// Parse `.sst` source into the unresolved AST. -// // pub fn parse_program( -// // source: &str, -// // ) -> eyre::Result>> -// // { -// // let (header, body) = parse_headers(source)?; -// // Ok(ParsedModule { -// // header, -// // functions: parse_functions(&body)?, -// // }) -// // } - -// pub fn compile_program(source: &str) -> eyre::Result { -// PPVMResolver::new().resolve_module(parse_program(source)?) -// } - -// /// Dump `.sst` source to a `.ssb` bytecode file. -// pub fn dump_program(program: &str, output_path: &str) -> eyre::Result<()> { -// let bytes = bytecode::compile_to_bytes(program)?; -// std::fs::write(output_path, bytes)?; -// Ok(()) -// } - -// /// Read a `.sst` file and dump it to a `.ssb` bytecode file. -// pub fn dump_file(input_path: &str, output_path: &str) -> eyre::Result<()> { -// let program = std::fs::read_to_string(input_path)?; -// dump_program(&program, output_path) -// } - pub mod prelude { pub use crate::component::Circuit; pub use crate::composite::PPVM; - // pub use crate::syntax::{PPVMHeader, PPVMResolver}; } - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[test] -// fn dump_program_writes_loadable_bytecode() { -// let src = "device circuit.n_qubits 1;\n\ -// fn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; -// let path = std::env::temp_dir().join("ppvm_dump_program_test.ssb"); -// dump_program(src, path.to_str().unwrap()).unwrap(); - -// let mut machine = PPVM::default(); -// machine.load_bytecode_file(path.to_str().unwrap()).unwrap(); -// machine.run().unwrap(); - -// assert_eq!(machine.measurement_record().len(), 1); -// let _ = std::fs::remove_file(&path); -// } - -// #[test] -// fn dump_file_reads_sst_and_writes_bytecode() { -// let out = std::env::temp_dir().join("ppvm_dump_file_test.ssb"); -// dump_file("tests/function_call.sst", out.to_str().unwrap()).unwrap(); - -// let mut machine = PPVM::default(); -// machine.load_bytecode_file(out.to_str().unwrap()).unwrap(); -// machine.run().unwrap(); - -// assert_eq!(machine.measurement_record().len(), 1); -// let _ = std::fs::remove_file(&out); -// } -// } From 7e13020f99f9bb306de780a48aa7170360508e42 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 10 Nov 2026 11:31:31 +0100 Subject: [PATCH 36/36] Fix clippy and machete errors --- Cargo.lock | 2 -- crates/ppvm-cli/src/commands.rs | 9 ++++----- crates/ppvm-tui/src/app.rs | 2 +- crates/ppvm-vihaco/Cargo.toml | 1 - crates/ppvm-vihaco/src/component/buckets.rs | 15 +++++++++------ crates/vihaco-circuit-isa/Cargo.toml | 1 - 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe0e211b5..0b0a0a41e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2409,7 +2409,6 @@ dependencies = [ "bnum", "chumsky 0.10.1", "eyre", - "log", "num", "ppvm-pauli-sum", "ppvm-tableau", @@ -3516,7 +3515,6 @@ dependencies = [ "eyre", "smallvec", "vihaco", - "vihaco-stdlib", ] [[package]] diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 1b2996943..fb909f837 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -6,7 +6,6 @@ use ppvm_vihaco::composite::{PPVM, PPVMModule}; use ppvm_vihaco::measurements::MeasurementResult; use ppvm_vihaco::shots::ShotRecord; use std::io::{BufRead, Write}; -use std::path::Path; /// Output format for `parse`. #[derive(Clone, Debug, clap::ValueEnum)] @@ -229,11 +228,11 @@ fn debug_loop( writeln!( output, "Measurements: {}", - format_shot(&machine.measurement_record()) + format_shot(machine.measurement_record()) )?; let traces = machine.trace_record(); if !traces.is_empty() { - writeln!(output, "Traces: {}", format_traces(&traces))?; + writeln!(output, "Traces: {}", format_traces(traces))?; } if !ever_paused { writeln!( @@ -254,11 +253,11 @@ fn print_location(machine: &PPVM, module: &PPVMModule, output: &mut impl Write) writeln!( output, "measurements: {}", - format_shot(&machine.measurement_record()) + format_shot(machine.measurement_record()) )?; let traces = machine.trace_record(); if !traces.is_empty() { - writeln!(output, "traces: {}", format_traces(&traces))?; + writeln!(output, "traces: {}", format_traces(traces))?; } Ok(()) } diff --git a/crates/ppvm-tui/src/app.rs b/crates/ppvm-tui/src/app.rs index 007ce14f2..38b67d700 100644 --- a/crates/ppvm-tui/src/app.rs +++ b/crates/ppvm-tui/src/app.rs @@ -357,7 +357,7 @@ impl AppState { if rec.is_empty() { "(none)".to_string() } else { - format_record(&rec) + format_record(rec) } } None => "(none)".to_string(), diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 4e7c419db..d0442aa2e 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -17,7 +17,6 @@ bitvec = "1.0.1" bnum = { version = "0.13.0", features = ["num-traits"] } chumsky = "0.10.0" eyre = "0.6.12" -log = "0.4.29" num = "0.4.3" rayon = { version = "1.10", optional = true } ppvm-tableau = { version = "0.3.0", path = "../ppvm-tableau" } diff --git a/crates/ppvm-vihaco/src/component/buckets.rs b/crates/ppvm-vihaco/src/component/buckets.rs index 216bedf14..a3fd0547f 100644 --- a/crates/ppvm-vihaco/src/component/buckets.rs +++ b/crates/ppvm-vihaco/src/component/buckets.rs @@ -13,14 +13,17 @@ use num::complex::Complex64; use ppvm_pauli_sum::config::fx64hash::Byte8F64; use ppvm_tableau::prelude::*; +pub(crate) type TableauExecutor = + CircuitExecutor, I, Vec<(Complex64, I)>>>; + // TODO: enum dispatch can go once we only set the index type in tableau #204 pub(crate) enum TableauBuckets { - Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>>), - Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>>), - Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>>), - Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>>), - Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>>), - Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>>), + Bits64(TableauExecutor<1, usize>), + Bits128(TableauExecutor<2, u128>), + Bits256(TableauExecutor<4, U256>), + Bits512(TableauExecutor<8, U512>), + Bits1024(TableauExecutor<16, U1024>), + Bits2048(TableauExecutor<32, U2048>), } impl TableauBuckets { diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index eccd7e38b..38154126a 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -11,4 +11,3 @@ chumsky = "0.10" eyre = "0.6.12" smallvec = "1.15.1" vihaco = { version = "0.7.0", features = ["program-image-execution"] } -vihaco-stdlib = { version = "0.7.0", features = ["program-image-execution"] }