diff --git a/Cargo.lock b/Cargo.lock index 55b321d4e..4085d9222 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,7 +408,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.1.14", + "unicode-width 0.2.0", ] [[package]] @@ -1488,7 +1488,7 @@ dependencies = [ "vihaco-circuit-isa", "vihaco-cpu", "vihaco-parser", - "vihaco-parser-core", + "vihaco-parser-derive", ] [[package]] @@ -1501,6 +1501,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2069,6 +2078,36 @@ dependencies = [ "serde_json", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db3bae107c9522f86d361697dee1d7386a2ddcf659d5aea5159819a21a3c4a7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "unarray" version = "0.1.4" @@ -2136,9 +2175,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vihaco" -version = "0.1.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "690c9db1827ed6e392340d3d9c65408213c7bb4c030b5e8991b03459e5c7bb17" +checksum = "8ca5492013a046a93fbc33c86b1a780c094cc1c7f330b1a863518abf2f5d9263" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -2148,9 +2187,51 @@ dependencies = [ "eyre", "log", "smallvec", - "vihaco-derive", + "vihaco-abi", + "vihaco-bytecode", + "vihaco-module", "vihaco-parser", - "vihaco-parser-core", + "vihaco-parser-derive", + "vihaco-runtime", + "vihaco-stdlib", + "vihaco-syntax", +] + +[[package]] +name = "vihaco-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce60d30d7bbdd0680a3e2f03602019a8e40e18ca0fdcf862fe7ad05ef8f253c" +dependencies = [ + "byteorder", + "chumsky 0.10.1", + "eyre", + "smallvec", + "vihaco-abi-derive", +] + +[[package]] +name = "vihaco-abi-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9e9ea31ac0f95b81b80039d9ad0efcbf613e7821a137cddad474ce35eee63" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vihaco-bytecode" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daab07fd1060f2d7480a83a2f0674c7047e972a25420a6896b8761a55de40c21" +dependencies = [ + "byteorder", + "chumsky 0.10.1", + "eyre", + "vihaco-abi", ] [[package]] @@ -2162,56 +2243,109 @@ dependencies = [ "smallvec", "vihaco", "vihaco-parser", - "vihaco-parser-core", + "vihaco-parser-derive", ] [[package]] name = "vihaco-cpu" -version = "0.1.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6cffffe3fca901bcddc389422dd9d2f7041038571a758a03c63e53ce1b6d5e" +checksum = "d43a9411dfe3802d58fec11952f33ab816a64ce1388742e1b2138b5768874072" dependencies = [ "chumsky 0.10.1", "codespan", "eyre", "log", "vihaco", - "vihaco-derive", "vihaco-parser", - "vihaco-parser-core", + "vihaco-parser-derive", ] [[package]] -name = "vihaco-derive" -version = "0.1.1" +name = "vihaco-module" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f11ac1123518d4bec265847a1a12e406c1b54f4ef2478db87f4e6986e26b918" +checksum = "7fb64aeec69cda6c683ed066db8466a4c3450c2380b02218ea2008ae1c26c227" dependencies = [ - "convert_case", + "colored", + "eyre", + "vihaco-abi", + "vihaco-bytecode", +] + +[[package]] +name = "vihaco-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7055d520235e111a3ab1b709b12598cb85b22af917a989249854faab2239e3c5" +dependencies = [ + "byteorder", + "chumsky 0.10.1", + "eyre", +] + +[[package]] +name = "vihaco-parser-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da6e8db950479302542e9327e51b9457ee8b029c3d68ff8b8dec827ddae757" +dependencies = [ + "chumsky 0.10.1", + "eyre", "proc-macro2", "quote", "syn", + "vihaco-parser", ] [[package]] -name = "vihaco-parser" -version = "0.1.1" +name = "vihaco-runtime" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b0c3b54b4caeaaeeda2953fa273155887e89ebdb0ce80b65d23c435b1e70a5" +checksum = "6f27c5c78eac004e23269c32967345901ebfd44f73b0cf4cdd018695c9fad176" dependencies = [ + "chumsky 0.10.1", + "eyre", + "vihaco-abi", + "vihaco-bytecode", + "vihaco-module", + "vihaco-parser", + "vihaco-runtime-derive", +] + +[[package]] +name = "vihaco-runtime-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec5b07b344dcad68f0d09c900056c8eea9f442b46daa1003d7420749314ee37" +dependencies = [ + "convert_case", + "proc-macro-crate", "proc-macro2", "quote", "syn", - "vihaco-parser-core", ] [[package]] -name = "vihaco-parser-core" -version = "0.1.1" +name = "vihaco-stdlib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd65a4e9145dc1a3c0ff004b146f960ce2971ed9c6ed9213e0db49627b21417e" +dependencies = [ + "eyre", + "vihaco-runtime", +] + +[[package]] +name = "vihaco-syntax" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea97e3bc67e4b5a97e21f2a4f64e4a87749038850968021ec903cc595353d20" +checksum = "bc1b62d87ca4a2a2eb225b6d00e62adaddd8b5b0c332ab044adf0b7c96d64fe3" dependencies = [ "chumsky 0.10.1", + "eyre", + "vihaco-bytecode", + "vihaco-parser", ] [[package]] @@ -2494,6 +2628,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index 722767aba..d5138403e 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -122,15 +122,15 @@ wherever you want execution to stop: ``` fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h // execution pauses here - breakpoint + cpu::cpu.breakpoint - const.u64 0 - circuit.measure - ret + cpu::cpu.const u64, 0 + circuit::circuit.measure + cpu::cpu.ret 0 } ``` @@ -145,6 +145,10 @@ measurements: Measurements: 0 ``` +The `next:` display shows the runtime instruction formatter (for example, +`const.u64`); `.sst` source files must use the qualified v0.4 spelling shown +above. + To step through a program that has no breakpoints, pass `-b`/`--break-at-start` to pause before the very first instruction: diff --git a/crates/ppvm-cli/examples/bit_flip_correction.sst b/crates/ppvm-cli/examples/bit_flip_correction.sst index a76d1e267..70352c1fa 100644 --- a/crates/ppvm-cli/examples/bit_flip_correction.sst +++ b/crates/ppvm-cli/examples/bit_flip_correction.sst @@ -3,64 +3,64 @@ device circuit.n_qubits 5; // Data: q0, q1, q2. // Syndrome ancillas: q3, q4. fn @main() { - const.u64 1 - const.f64 0.25 - const.f64 0.0 - const.f64 0.0 - circuit.paulierror + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.25 + cpu::cpu.const f64, 0.0 + cpu::cpu.const f64, 0.0 + circuit::circuit.paulierror - const.u64 0 - const.u64 3 - circuit.cnot - const.u64 1 - const.u64 3 - circuit.cnot - const.u64 3 - circuit.measure + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 3 + circuit::circuit.cnot + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 3 + circuit::circuit.cnot + cpu::cpu.const u64, 3 + circuit::circuit.measure - const.u64 1 - const.u64 4 - circuit.cnot - const.u64 2 - const.u64 4 - circuit.cnot - const.u64 4 - circuit.measure + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 4 + circuit::circuit.cnot + cpu::cpu.const u64, 2 + cpu::cpu.const u64, 4 + circuit::circuit.cnot + cpu::cpu.const u64, 4 + circuit::circuit.measure - const.u32 1 - eq.u32 - cond_br @s12_one, @s12_zero + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @s12_one, @s12_zero -@s12_one: - const.u32 1 - eq.u32 - cond_br @correct_q1, @correct_q2 +cpu::cpu.label @s12_one + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @correct_q1, @correct_q2 -@s12_zero: - const.u32 1 - eq.u32 - cond_br @correct_q0, @readout +cpu::cpu.label @s12_zero + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @correct_q0, @readout -@correct_q0: - const.u64 0 - circuit.x - br @readout +cpu::cpu.label @correct_q0 + cpu::cpu.const u64, 0 + circuit::circuit.x + cpu::cpu.br @readout -@correct_q1: - const.u64 1 - circuit.x - br @readout +cpu::cpu.label @correct_q1 + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @readout -@correct_q2: - const.u64 2 - circuit.x +cpu::cpu.label @correct_q2 + cpu::cpu.const u64, 2 + circuit::circuit.x -@readout: - const.u64 0 - circuit.measure - const.u64 1 - circuit.measure - const.u64 2 - circuit.measure - ret +cpu::cpu.label @readout + cpu::cpu.const u64, 0 + circuit::circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.const u64, 2 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/ghz.sst b/crates/ppvm-cli/examples/ghz.sst index aa88b8a65..7be5f2e3b 100644 --- a/crates/ppvm-cli/examples/ghz.sst +++ b/crates/ppvm-cli/examples/ghz.sst @@ -3,25 +3,25 @@ device circuit.n_qubits 3; // Prepare a 3-qubit GHZ state (|000> + |111>)/sqrt(2) and measure every qubit. // The three outcomes are perfectly correlated, so each shot reads 0 0 0 or 1 1 1. fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 1 - const.u64 2 - circuit.cnot + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 2 + circuit::circuit.cnot - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure - const.u64 2 - circuit.measure + cpu::cpu.const u64, 2 + circuit::circuit.measure - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/heisenberg_zz.sst b/crates/ppvm-cli/examples/heisenberg_zz.sst index 028e7e098..4aad28632 100644 --- a/crates/ppvm-cli/examples/heisenberg_zz.sst +++ b/crates/ppvm-cli/examples/heisenberg_zz.sst @@ -11,7 +11,7 @@ device circuit.coefficient_threshold 1e-10; // sum directly: 1.0 + 0.5 = 1.5. Add `circuit.cnot; circuit.h; circuit.truncate` // in textbook-reversed order to evolve before tracing. fn @main() { - const.str "[XZ]?*" - circuit.trace - ret + cpu::cpu.const str, "[XZ]?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/loop_feedforward.sst b/crates/ppvm-cli/examples/loop_feedforward.sst index f1a7f6d87..0446467a8 100644 --- a/crates/ppvm-cli/examples/loop_feedforward.sst +++ b/crates/ppvm-cli/examples/loop_feedforward.sst @@ -8,37 +8,37 @@ device circuit.n_qubits 2; // prints 4 bits: q0_round1 q0_round2 q0_round3 q1. fn @main() { // Loop counter starts at 0 and lives on the bottom of the stack. - const.u64 0 + cpu::cpu.const u64, 0 -@loop: +cpu::cpu.label @loop // Flip a fair coin: H then measure q0 (pushes outcome 0/1 as u32). - const.u64 0 - circuit.h - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.measure // Feed-forward: if the outcome was 1, apply X to q1. - const.u32 1 - eq.u32 - cond_br @flip, @next + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @flip, @next -@flip: - const.u64 1 - circuit.x - br @next +cpu::cpu.label @flip + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @next -@next: +cpu::cpu.label @next // counter += 1, then loop while counter < 3. - const.u64 1 - add.u64 - dup - const.u64 3 - lt.u64 - cond_br @loop, @done + cpu::cpu.const u64, 1 + cpu::cpu.add u64 + cpu::cpu.dup + cpu::cpu.const u64, 3 + cpu::cpu.lt u64 + cpu::cpu.cond_br @loop, @done -@done: +cpu::cpu.label @done // Final readout of q1. - const.u64 1 - circuit.measure - ret + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/simple_loop.sst b/crates/ppvm-cli/examples/simple_loop.sst index 3591973ac..c68bd0d1b 100644 --- a/crates/ppvm-cli/examples/simple_loop.sst +++ b/crates/ppvm-cli/examples/simple_loop.sst @@ -1,33 +1,33 @@ device circuit.n_qubits 1; fn @main() { - const.u64 0 + cpu::cpu.const u64, 0 -@loop: - const.u64 0 - circuit.h - const.u64 0 - circuit.measure +cpu::cpu.label @loop + cpu::cpu.const u64, 0 + circuit::circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.measure // stop here to investigate - breakpoint + cpu::cpu.breakpoint - const.u32 1 - eq.u32 - cond_br @flip, @next + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @flip, @next -@flip: - const.u64 0 - circuit.x +cpu::cpu.label @flip + cpu::cpu.const u64, 0 + circuit::circuit.x -@next: - const.u64 1 - add.u64 - dup - const.u64 2 - lt.u64 - cond_br @loop, @done +cpu::cpu.label @next + cpu::cpu.const u64, 1 + cpu::cpu.add u64 + cpu::cpu.dup + cpu::cpu.const u64, 2 + cpu::cpu.lt u64 + cpu::cpu.cond_br @loop, @done -@done: - ret +cpu::cpu.label @done + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 574d3c174..9ffd2d1c7 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -120,8 +120,8 @@ pub fn parse(file: &str, format: Format) -> Result<()> { } Format::Pretty => { println!("Module:"); - println!(" Headers: {}", parsed.headers.len()); - for (i, header) in parsed.headers.iter().enumerate() { + println!(" Headers: {}", parsed.header.len()); + for (i, header) in parsed.header.iter().enumerate() { println!(" [{}] {:?}", i, header); } println!(" Functions: {}", parsed.functions.len()); @@ -169,7 +169,7 @@ enum DebugCommand { Quit, } -/// Step through a program interactively, pausing at `breakpoint` instructions. +/// 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. pub fn debug(file: &str, break_at_start: bool) -> Result<()> { @@ -219,7 +219,7 @@ fn debug_loop( StepOutcome::Breakpoint => { paused = true; ever_paused = true; - writeln!(output, "-- breakpoint hit --")?; + writeln!(output, "-- cpu::cpu.breakpoint hit --")?; } StepOutcome::Return | StepOutcome::Halt => { writeln!(output, "Program finished.")?; @@ -240,7 +240,7 @@ fn debug_loop( if !ever_paused { writeln!( output, - "(no breakpoint was hit; pass --break-at-start to step from the beginning)" + "(no cpu::cpu.breakpoint was hit; pass --break-at-start to step from the beginning)" )?; } Ok(()) @@ -292,8 +292,7 @@ mod tests { use std::fs; /// Minimal program that compiles and measures q0 in |0> (deterministic). - const PROGRAM: &str = - "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit.measure\n ret }\n"; + 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"; fn row(outcomes: &[MeasurementOutcome]) -> MeasurementResult { outcomes.iter().copied().collect() @@ -411,7 +410,7 @@ mod tests { const TRACE_PROGRAM: &str = "device circuit.n_qubits 1;\n\ device circuit.backend paulisum;\n\ device circuit.observable Z;\n\ - fn @main() { const.str \"Z?*\"\n circuit.trace\n ret }\n"; + fn @main() { cpu::cpu.const str, \"Z?*\"\n circuit::circuit.trace\n cpu::cpu.ret 0 }\n"; 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); @@ -513,8 +512,8 @@ mod tests { // ─── debug ───────────────────────────────────────────────────────── - /// Program with a `breakpoint` before measuring q0 in |0> (deterministic). - const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + /// 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"; /// 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,7 +527,7 @@ mod tests { #[test] fn debug_break_at_start_steps_through_to_finish() { - // PROGRAM is const.u64 0 / circuit.measure / ret = 3 steps. + // 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"); assert!( out.contains("next: Measure"), @@ -550,12 +549,12 @@ mod tests { #[test] fn debug_honors_authored_breakpoint() { - // Not breaking at start: must run until the `breakpoint` pauses it. + // Not breaking at start: must run until the `cpu::cpu.breakpoint` pauses it. let out = run_debug(BREAKPOINT_PROGRAM, "ppvm_cli_debug_bp.sst", false, "c\n"); - assert!(out.contains("-- breakpoint hit --"), "{out}"); + assert!(out.contains("-- cpu::cpu.breakpoint hit --"), "{out}"); assert!(out.contains("Program finished."), "{out}"); - // A breakpoint was hit, so no "use --break-at-start" hint. - assert!(!out.contains("no breakpoint was hit"), "{out}"); + // A cpu::cpu.breakpoint was hit, so no "use --break-at-start" hint. + assert!(!out.contains("no cpu::cpu.breakpoint was hit"), "{out}"); } #[test] @@ -571,10 +570,10 @@ mod tests { #[test] fn debug_without_breakpoint_prints_hint() { - // No breakpoint, no break-at-start, empty input: runs straight through + // No cpu::cpu.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 breakpoint was hit"), "{out}"); + assert!(out.contains("no cpu::cpu.breakpoint was hit"), "{out}"); } } diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index ffa73f6ea..2d8062b97 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -78,7 +78,7 @@ enum Commands { format: commands::MeasurementFormat, }, - /// Step through a program interactively, pausing at `breakpoint` instructions + /// Step through a program interactively, pausing at `cpu::cpu.breakpoint` instructions Debug { /// Input file (.sst source or .ssb bytecode) #[arg(value_name = "FILE")] diff --git a/crates/ppvm-tui/src/app.rs b/crates/ppvm-tui/src/app.rs index 215f4f21c..d4a768a76 100644 --- a/crates/ppvm-tui/src/app.rs +++ b/crates/ppvm-tui/src/app.rs @@ -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 breakpoint). + /// a step, or at a cpu::cpu.breakpoint). paused: bool, /// True once the loaded program has run to Return/Halt. finished: bool, @@ -236,7 +236,7 @@ impl AppState { } StepOutcome::Breakpoint => { self.paused = true; - self.set_status("-- breakpoint hit --"); + self.set_status("-- cpu::cpu.breakpoint hit --"); } StepOutcome::Return | StepOutcome::Halt => { self.paused = false; @@ -459,7 +459,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 breakpoint or the end + :continue :c run to the next cpu::cpu.breakpoint or the end :reset restart the loaded program / device :help :h toggle this help :quit :q (Ctrl-C) leave @@ -471,7 +471,7 @@ Gates (q = qubit index; angles / probabilities are floats) u3 rxx ryy rzz depolarize loss

depolarize2

- paulierror correlatedloss + pauli_error correlated_loss Line editing: ←/→ move · Home/End · Backspace/Del · ↑/↓ history"; @@ -602,9 +602,9 @@ mod tests { assert!(app.should_exit); } - /// A 1-qubit program with a breakpoint before measuring q0 (|0> -> 0). + /// 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() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; #[test] fn load_source_starts_paused_with_a_listing() { @@ -624,7 +624,7 @@ mod tests { app.load_source(BP_PROGRAM).unwrap(); app.dispatch(":c"); assert!( - app.status().contains("breakpoint"), + app.status().contains("cpu::cpu.breakpoint"), "status: {}", app.status() ); @@ -642,7 +642,7 @@ mod tests { fn finishing_clears_paused() { let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); - app.dispatch(":c"); // pause at breakpoint + app.dispatch(":c"); // pause at cpu::cpu.breakpoint assert!(app.paused()); app.dispatch(":c"); // run to Return assert!(!app.paused(), "paused must clear once the program finishes"); @@ -652,7 +652,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 breakpoint + app.dispatch(":c"); // pause at cpu::cpu.breakpoint app.dispatch(":c"); // run to Return assert!( !app.hint().contains("step") && !app.hint().contains("continue"), @@ -686,11 +686,11 @@ mod tests { #[test] fn inject_gate_at_breakpoint_then_resume() { - // At the breakpoint, inject X on q0; resuming, the program measures |1>. + // At the cpu::cpu.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 breakpoint - assert!(app.status().contains("breakpoint")); + app.dispatch(":c"); // run to the cpu::cpu.breakpoint + assert!(app.status().contains("cpu::cpu.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 0cea9ba15..75fc30000 100644 --- a/crates/ppvm-tui/src/command.rs +++ b/crates/ppvm-tui/src/command.rs @@ -48,8 +48,8 @@ pub fn gate_spec(name: &str) -> Option { "depolarize" => (Depolarize, 1, 1), "depolarize2" => (Depolarize2, 2, 1), "loss" => (Loss, 1, 1), - "paulierror" => (PauliError, 1, 3), - "correlatedloss" => (CorrelatedLoss, 2, 3), + "pauli_error" => (PauliError, 1, 3), + "correlated_loss" => (CorrelatedLoss, 2, 3), _ => return None, }; Some(GateSpec { @@ -72,7 +72,7 @@ pub enum Command { }, /// Advance one instruction (also the meaning of an empty line). Step, - /// Run to the next breakpoint or program end. + /// Run to the next cpu::cpu.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 f0dcf0cea..5a18a1d03 100644 --- a/crates/ppvm-tui/src/widgets.rs +++ b/crates/ppvm-tui/src/widgets.rs @@ -93,7 +93,7 @@ mod tests { use ratatui::backend::TestBackend; const BP_PROGRAM: &str = "device circuit.n_qubits 1;\n\ - fn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; #[test] fn renders_all_panels_without_panic() { diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 9abb13991..fa9ecd110 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -19,9 +19,9 @@ num = "0.4.3" rayon = { version = "1.10", optional = true } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau" } smallvec = "1.15.1" -vihaco = "0.1.1" -vihaco-cpu = "0.1.1" -vihaco-parser = "0.1.1" -vihaco-parser-core = "0.1.1" +vihaco = "0.4.0" +vihaco-cpu = "0.4.0" +vihaco-parser = "0.4.0" +vihaco-parser-derive = "0.4.0" vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" } ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" } diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index ef1153716..bd2023bba 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -12,12 +12,362 @@ use std::io::{Read, Write}; use vihaco::instruction::{FromBytes, WriteBytes}; +use vihaco::module::{FunctionInfo, LabelInfo, Parameter, Signature}; +use vihaco::{Type, Value}; +use vihaco_cpu::RuntimeInstruction as CpuInstruction; use crate::PPVMModule; use crate::composite::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo, PPVMInstruction}; +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeType { + Undefined, + String, + Bool, + I64, + U32, + U64, + F64, + FunctionRef, + HeapRef, +} + +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeValue { + Undefined, + String(u32), + Bool(bool), + I64(i64), + U32(u32), + U64(u64), + F64(f64), + FunctionRef(u32), + HeapRef(u32), +} + +fn encode_type(value: Type) -> BytecodeType { + match value { + Type::Undefined => BytecodeType::Undefined, + Type::String => BytecodeType::String, + Type::Bool => BytecodeType::Bool, + Type::I64 => BytecodeType::I64, + Type::U32 => BytecodeType::U32, + Type::U64 => BytecodeType::U64, + Type::F64 => BytecodeType::F64, + Type::FunctionRef => BytecodeType::FunctionRef, + Type::HeapRef => BytecodeType::HeapRef, + } +} + +fn decode_type(value: BytecodeType) -> Type { + match value { + BytecodeType::Undefined => Type::Undefined, + BytecodeType::String => Type::String, + BytecodeType::Bool => Type::Bool, + BytecodeType::I64 => Type::I64, + BytecodeType::U32 => Type::U32, + BytecodeType::U64 => Type::U64, + BytecodeType::F64 => Type::F64, + BytecodeType::FunctionRef => Type::FunctionRef, + BytecodeType::HeapRef => Type::HeapRef, + } +} + +fn encode_value(value: Value) -> BytecodeValue { + match value { + Value::Undefined => BytecodeValue::Undefined, + Value::String(v) => BytecodeValue::String(v), + Value::Bool(v) => BytecodeValue::Bool(v), + Value::I64(v) => BytecodeValue::I64(v), + Value::U32(v) => BytecodeValue::U32(v), + Value::U64(v) => BytecodeValue::U64(v), + Value::F64(v) => BytecodeValue::F64(v), + Value::FunctionRef(v) => BytecodeValue::FunctionRef(v), + Value::HeapRef(v) => BytecodeValue::HeapRef(v), + } +} + +fn decode_value(value: BytecodeValue) -> Value { + match value { + BytecodeValue::Undefined => Value::Undefined, + BytecodeValue::String(v) => Value::String(v), + BytecodeValue::Bool(v) => Value::Bool(v), + BytecodeValue::I64(v) => Value::I64(v), + BytecodeValue::U32(v) => Value::U32(v), + BytecodeValue::U64(v) => Value::U64(v), + BytecodeValue::F64(v) => Value::F64(v), + BytecodeValue::FunctionRef(v) => Value::FunctionRef(v), + BytecodeValue::HeapRef(v) => Value::HeapRef(v), + } +} + +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeCpu { + Span(u32, u32, u32), + FunctionStart, + FunctionEnd, + Breakpoint, + Branch(u32), + ConditionalBranch(u32, u32), + Return(u32), + IndirectCall, + Call(u32, u32), + Halt, + Print, + Load(BytecodeType, u32), + Store(BytecodeType, u32), + Dup, + HeapAlloc(u32), + GetItem, + HeapDealloc, + Const(BytecodeType, BytecodeValue), + Add(BytecodeType), + Sub(BytecodeType), + Mul(BytecodeType), + Div(BytecodeType), + Rem(BytecodeType), + Neg(BytecodeType), + Shl(BytecodeType), + Shr(BytecodeType), + Rol(BytecodeType), + Ror(BytecodeType), + BitAnd(BytecodeType), + BitOr(BytecodeType), + BitXor(BytecodeType), + Not, + And, + Or, + Xor, + Eq(BytecodeType), + Ne(BytecodeType), + Lt(BytecodeType), + Gt(BytecodeType), + Le(BytecodeType), + Ge(BytecodeType), +} + +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeCircuit { + TwoQubitPauliError, + Truncate, + Trace, + X, + Y, + Z, + H, + SqrtXAdj, + SqrtX, + SqrtYAdj, + SqrtY, + SAdj, + S, + CNOT, + CZ, + TAdj, + T, + RXX, + RYY, + RZZ, + RX, + RY, + RZ, + U3, + Measure, + Reset, + R, + Loss, + CorrelatedLoss, + PauliError, + Depolarize2, + Depolarize, +} + +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeInstruction { + Cpu(BytecodeCpu), + Circuit(BytecodeCircuit), +} + +fn encode_instruction(inst: &PPVMInstruction) -> eyre::Result { + let encoded = match inst { + PPVMInstruction::Cpu(inst) => BytecodeInstruction::Cpu(match inst { + CpuInstruction::Span(a, b, c) => BytecodeCpu::Span(*a, *b, *c), + CpuInstruction::FunctionStart => BytecodeCpu::FunctionStart, + CpuInstruction::FunctionEnd => BytecodeCpu::FunctionEnd, + CpuInstruction::Breakpoint => BytecodeCpu::Breakpoint, + CpuInstruction::Branch(v) => BytecodeCpu::Branch(*v), + CpuInstruction::ConditionalBranch(a, b) => BytecodeCpu::ConditionalBranch(*a, *b), + CpuInstruction::Return(v) => BytecodeCpu::Return(*v), + CpuInstruction::IndirectCall => BytecodeCpu::IndirectCall, + CpuInstruction::Call(a, b) => BytecodeCpu::Call(*a, *b), + CpuInstruction::Halt => BytecodeCpu::Halt, + CpuInstruction::Print => BytecodeCpu::Print, + CpuInstruction::Load(t, v) => BytecodeCpu::Load(encode_type(*t), *v), + CpuInstruction::Store(t, v) => BytecodeCpu::Store(encode_type(*t), *v), + CpuInstruction::Dup => BytecodeCpu::Dup, + CpuInstruction::HeapAlloc(v) => BytecodeCpu::HeapAlloc(*v), + CpuInstruction::GetItem => BytecodeCpu::GetItem, + CpuInstruction::HeapDealloc => BytecodeCpu::HeapDealloc, + CpuInstruction::Const(t, v) => BytecodeCpu::Const(encode_type(*t), encode_value(*v)), + CpuInstruction::Add(t) => BytecodeCpu::Add(encode_type(*t)), + CpuInstruction::Sub(t) => BytecodeCpu::Sub(encode_type(*t)), + CpuInstruction::Mul(t) => BytecodeCpu::Mul(encode_type(*t)), + CpuInstruction::Div(t) => BytecodeCpu::Div(encode_type(*t)), + CpuInstruction::Rem(t) => BytecodeCpu::Rem(encode_type(*t)), + CpuInstruction::Neg(t) => BytecodeCpu::Neg(encode_type(*t)), + CpuInstruction::Shl(t) => BytecodeCpu::Shl(encode_type(*t)), + CpuInstruction::Shr(t) => BytecodeCpu::Shr(encode_type(*t)), + CpuInstruction::Rol(t) => BytecodeCpu::Rol(encode_type(*t)), + CpuInstruction::Ror(t) => BytecodeCpu::Ror(encode_type(*t)), + CpuInstruction::BitAnd(t) => BytecodeCpu::BitAnd(encode_type(*t)), + CpuInstruction::BitOr(t) => BytecodeCpu::BitOr(encode_type(*t)), + CpuInstruction::BitXor(t) => BytecodeCpu::BitXor(encode_type(*t)), + CpuInstruction::Not => BytecodeCpu::Not, + CpuInstruction::And => BytecodeCpu::And, + CpuInstruction::Or => BytecodeCpu::Or, + CpuInstruction::Xor => BytecodeCpu::Xor, + CpuInstruction::Eq(t) => BytecodeCpu::Eq(encode_type(*t)), + CpuInstruction::Ne(t) => BytecodeCpu::Ne(encode_type(*t)), + CpuInstruction::Lt(t) => BytecodeCpu::Lt(encode_type(*t)), + CpuInstruction::Gt(t) => BytecodeCpu::Gt(encode_type(*t)), + CpuInstruction::Le(t) => BytecodeCpu::Le(encode_type(*t)), + CpuInstruction::Ge(t) => BytecodeCpu::Ge(encode_type(*t)), + CpuInstruction::Label(_) => { + return Err(eyre::eyre!( + "runtime labels are not serializable in PPVM bytecode" + )); + } + }), + PPVMInstruction::Circuit(inst) => BytecodeInstruction::Circuit(match inst { + vihaco_circuit_isa::CircuitInstruction::TwoQubitPauliError => { + BytecodeCircuit::TwoQubitPauliError + } + vihaco_circuit_isa::CircuitInstruction::Truncate => BytecodeCircuit::Truncate, + vihaco_circuit_isa::CircuitInstruction::Trace => BytecodeCircuit::Trace, + vihaco_circuit_isa::CircuitInstruction::X => BytecodeCircuit::X, + vihaco_circuit_isa::CircuitInstruction::Y => BytecodeCircuit::Y, + vihaco_circuit_isa::CircuitInstruction::Z => BytecodeCircuit::Z, + vihaco_circuit_isa::CircuitInstruction::H => BytecodeCircuit::H, + vihaco_circuit_isa::CircuitInstruction::SqrtXAdj => BytecodeCircuit::SqrtXAdj, + vihaco_circuit_isa::CircuitInstruction::SqrtX => BytecodeCircuit::SqrtX, + vihaco_circuit_isa::CircuitInstruction::SqrtYAdj => BytecodeCircuit::SqrtYAdj, + vihaco_circuit_isa::CircuitInstruction::SqrtY => BytecodeCircuit::SqrtY, + vihaco_circuit_isa::CircuitInstruction::SAdj => BytecodeCircuit::SAdj, + vihaco_circuit_isa::CircuitInstruction::S => BytecodeCircuit::S, + vihaco_circuit_isa::CircuitInstruction::CNOT => BytecodeCircuit::CNOT, + vihaco_circuit_isa::CircuitInstruction::CZ => BytecodeCircuit::CZ, + vihaco_circuit_isa::CircuitInstruction::TAdj => BytecodeCircuit::TAdj, + vihaco_circuit_isa::CircuitInstruction::T => BytecodeCircuit::T, + vihaco_circuit_isa::CircuitInstruction::RXX => BytecodeCircuit::RXX, + vihaco_circuit_isa::CircuitInstruction::RYY => BytecodeCircuit::RYY, + vihaco_circuit_isa::CircuitInstruction::RZZ => BytecodeCircuit::RZZ, + vihaco_circuit_isa::CircuitInstruction::RX => BytecodeCircuit::RX, + vihaco_circuit_isa::CircuitInstruction::RY => BytecodeCircuit::RY, + vihaco_circuit_isa::CircuitInstruction::RZ => BytecodeCircuit::RZ, + vihaco_circuit_isa::CircuitInstruction::U3 => BytecodeCircuit::U3, + vihaco_circuit_isa::CircuitInstruction::Measure => BytecodeCircuit::Measure, + vihaco_circuit_isa::CircuitInstruction::Reset => BytecodeCircuit::Reset, + vihaco_circuit_isa::CircuitInstruction::R => BytecodeCircuit::R, + vihaco_circuit_isa::CircuitInstruction::Loss => BytecodeCircuit::Loss, + vihaco_circuit_isa::CircuitInstruction::CorrelatedLoss => { + BytecodeCircuit::CorrelatedLoss + } + vihaco_circuit_isa::CircuitInstruction::PauliError => BytecodeCircuit::PauliError, + vihaco_circuit_isa::CircuitInstruction::Depolarize2 => BytecodeCircuit::Depolarize2, + vihaco_circuit_isa::CircuitInstruction::Depolarize => BytecodeCircuit::Depolarize, + }), + }; + Ok(encoded) +} + +fn decode_instruction(inst: BytecodeInstruction) -> PPVMInstruction { + match inst { + BytecodeInstruction::Cpu(inst) => PPVMInstruction::Cpu(match inst { + BytecodeCpu::Span(a, b, c) => CpuInstruction::Span(a, b, c), + BytecodeCpu::FunctionStart => CpuInstruction::FunctionStart, + BytecodeCpu::FunctionEnd => CpuInstruction::FunctionEnd, + BytecodeCpu::Breakpoint => CpuInstruction::Breakpoint, + BytecodeCpu::Branch(v) => CpuInstruction::Branch(v), + BytecodeCpu::ConditionalBranch(a, b) => CpuInstruction::ConditionalBranch(a, b), + BytecodeCpu::Return(v) => CpuInstruction::Return(v), + BytecodeCpu::IndirectCall => CpuInstruction::IndirectCall, + BytecodeCpu::Call(a, b) => CpuInstruction::Call(a, b), + BytecodeCpu::Halt => CpuInstruction::Halt, + BytecodeCpu::Print => CpuInstruction::Print, + BytecodeCpu::Load(t, v) => CpuInstruction::Load(decode_type(t), v), + BytecodeCpu::Store(t, v) => CpuInstruction::Store(decode_type(t), v), + BytecodeCpu::Dup => CpuInstruction::Dup, + BytecodeCpu::HeapAlloc(v) => CpuInstruction::HeapAlloc(v), + BytecodeCpu::GetItem => CpuInstruction::GetItem, + BytecodeCpu::HeapDealloc => CpuInstruction::HeapDealloc, + BytecodeCpu::Const(t, v) => CpuInstruction::Const(decode_type(t), decode_value(v)), + BytecodeCpu::Add(t) => CpuInstruction::Add(decode_type(t)), + BytecodeCpu::Sub(t) => CpuInstruction::Sub(decode_type(t)), + BytecodeCpu::Mul(t) => CpuInstruction::Mul(decode_type(t)), + BytecodeCpu::Div(t) => CpuInstruction::Div(decode_type(t)), + BytecodeCpu::Rem(t) => CpuInstruction::Rem(decode_type(t)), + BytecodeCpu::Neg(t) => CpuInstruction::Neg(decode_type(t)), + BytecodeCpu::Shl(t) => CpuInstruction::Shl(decode_type(t)), + BytecodeCpu::Shr(t) => CpuInstruction::Shr(decode_type(t)), + BytecodeCpu::Rol(t) => CpuInstruction::Rol(decode_type(t)), + BytecodeCpu::Ror(t) => CpuInstruction::Ror(decode_type(t)), + BytecodeCpu::BitAnd(t) => CpuInstruction::BitAnd(decode_type(t)), + BytecodeCpu::BitOr(t) => CpuInstruction::BitOr(decode_type(t)), + BytecodeCpu::BitXor(t) => CpuInstruction::BitXor(decode_type(t)), + BytecodeCpu::Not => CpuInstruction::Not, + BytecodeCpu::And => CpuInstruction::And, + BytecodeCpu::Or => CpuInstruction::Or, + BytecodeCpu::Xor => CpuInstruction::Xor, + BytecodeCpu::Eq(t) => CpuInstruction::Eq(decode_type(t)), + BytecodeCpu::Ne(t) => CpuInstruction::Ne(decode_type(t)), + BytecodeCpu::Lt(t) => CpuInstruction::Lt(decode_type(t)), + BytecodeCpu::Gt(t) => CpuInstruction::Gt(decode_type(t)), + BytecodeCpu::Le(t) => CpuInstruction::Le(decode_type(t)), + BytecodeCpu::Ge(t) => CpuInstruction::Ge(decode_type(t)), + }), + BytecodeInstruction::Circuit(inst) => PPVMInstruction::Circuit(match inst { + BytecodeCircuit::TwoQubitPauliError => { + vihaco_circuit_isa::CircuitInstruction::TwoQubitPauliError + } + BytecodeCircuit::Truncate => vihaco_circuit_isa::CircuitInstruction::Truncate, + BytecodeCircuit::Trace => vihaco_circuit_isa::CircuitInstruction::Trace, + BytecodeCircuit::X => vihaco_circuit_isa::CircuitInstruction::X, + BytecodeCircuit::Y => vihaco_circuit_isa::CircuitInstruction::Y, + BytecodeCircuit::Z => vihaco_circuit_isa::CircuitInstruction::Z, + BytecodeCircuit::H => vihaco_circuit_isa::CircuitInstruction::H, + BytecodeCircuit::SqrtXAdj => vihaco_circuit_isa::CircuitInstruction::SqrtXAdj, + BytecodeCircuit::SqrtX => vihaco_circuit_isa::CircuitInstruction::SqrtX, + BytecodeCircuit::SqrtYAdj => vihaco_circuit_isa::CircuitInstruction::SqrtYAdj, + BytecodeCircuit::SqrtY => vihaco_circuit_isa::CircuitInstruction::SqrtY, + BytecodeCircuit::SAdj => vihaco_circuit_isa::CircuitInstruction::SAdj, + BytecodeCircuit::S => vihaco_circuit_isa::CircuitInstruction::S, + BytecodeCircuit::CNOT => vihaco_circuit_isa::CircuitInstruction::CNOT, + BytecodeCircuit::CZ => vihaco_circuit_isa::CircuitInstruction::CZ, + BytecodeCircuit::TAdj => vihaco_circuit_isa::CircuitInstruction::TAdj, + BytecodeCircuit::T => vihaco_circuit_isa::CircuitInstruction::T, + BytecodeCircuit::RXX => vihaco_circuit_isa::CircuitInstruction::RXX, + BytecodeCircuit::RYY => vihaco_circuit_isa::CircuitInstruction::RYY, + BytecodeCircuit::RZZ => vihaco_circuit_isa::CircuitInstruction::RZZ, + BytecodeCircuit::RX => vihaco_circuit_isa::CircuitInstruction::RX, + BytecodeCircuit::RY => vihaco_circuit_isa::CircuitInstruction::RY, + BytecodeCircuit::RZ => vihaco_circuit_isa::CircuitInstruction::RZ, + BytecodeCircuit::U3 => vihaco_circuit_isa::CircuitInstruction::U3, + BytecodeCircuit::Measure => vihaco_circuit_isa::CircuitInstruction::Measure, + BytecodeCircuit::Reset => vihaco_circuit_isa::CircuitInstruction::Reset, + BytecodeCircuit::R => vihaco_circuit_isa::CircuitInstruction::R, + BytecodeCircuit::Loss => vihaco_circuit_isa::CircuitInstruction::Loss, + BytecodeCircuit::CorrelatedLoss => { + vihaco_circuit_isa::CircuitInstruction::CorrelatedLoss + } + BytecodeCircuit::PauliError => vihaco_circuit_isa::CircuitInstruction::PauliError, + BytecodeCircuit::Depolarize2 => vihaco_circuit_isa::CircuitInstruction::Depolarize2, + BytecodeCircuit::Depolarize => vihaco_circuit_isa::CircuitInstruction::Depolarize, + }), + } +} + /// Current `.ssb` format version. The reader rejects any other version. -pub const PPVM_BYTECODE_VERSION: u16 = 1; +pub const PPVM_BYTECODE_VERSION: u16 = 2; /// Byte length of the fixed portion of the header. The actual `header_size` /// in the stream may exceed this when the optional `observable` string is @@ -28,28 +378,11 @@ pub const PPVM_BYTECODE_VERSION: u16 = 1; /// + max_pauli_weight(8) + observable_present(1) = 33. const FIXED_HEADER_SIZE: u32 = 4 + 2 + 4 + 4 + 8 + 1 + 1 + 8 + 1; -/// Serialize a resolved module to the v1 `.ssb` byte stream. +/// Serialize a resolved module to the v2 `.ssb` byte stream. pub fn write_module(module: &PPVMModule, w: &mut W) -> eyre::Result<()> { - // v1 serializes only code, strings, and device info. Refuse to silently - // drop any table a future feature might populate. - let populated = if !module.functions.is_empty() { - Some("functions") - } else if !module.labels.is_empty() { - Some("labels") - } else if !module.constants.is_empty() { - Some("constants") - } else if !module.source_symbols.is_empty() { - Some("source_symbols") - } else if module.main_function.is_some() { - Some("main_function") - } else if module.file != 0 { - Some("file") - } else { - None - }; - if let Some(table) = populated { + if !module.constants.is_empty() || !module.source_symbols.is_empty() { return Err(eyre::eyre!( - "bytecode v1 cannot represent a populated `{table}`" + "bytecode v2 cannot represent constants or source symbols" )); } @@ -111,18 +444,44 @@ pub fn write_module(module: &PPVMModule, w: &mut W) -> eyre::Result<() w.write_all(s.as_bytes())?; } + // Metadata section: function and label tables are required for calls and + // branches to retain their resolved targets after a bytecode round trip. + write_u32(w, u32::try_from(module.functions.len())?)?; + for function in &module.functions { + write_u32(w, function.name)?; + write_u32(w, function.local_count)?; + write_u32(w, function.start_address)?; + write_u32(w, function.end_address)?; + write_u32(w, function.file)?; + write_u32(w, u32::try_from(function.signature.params.len())?)?; + for parameter in &function.signature.params { + write_u32(w, parameter.name)?; + parameter.ty.write_bytes(w)?; + } + write_u32(w, u32::try_from(function.signature.ret.len())?)?; + for ty in &function.signature.ret { + ty.write_bytes(w)?; + } + } + write_u32(w, u32::try_from(module.labels.len())?)?; + for label in &module.labels { + write_u32(w, label.address)?; + write_u32(w, label.name)?; + } + write_u32(w, module.main_function.unwrap_or(u32::MAX))?; + // Code section: count, then each instruction's fixed-width frame. let code_count = u32::try_from(module.code.len()).map_err(|_| eyre::eyre!("code length exceeds u32"))?; w.write_all(&code_count.to_le_bytes())?; for inst in &module.code { - inst.write_bytes(w)?; + encode_instruction(inst)?.write_bytes(w)?; } Ok(()) } -/// Reconstruct a module from a v1 `.ssb` byte stream. +/// Reconstruct a module from a v2 `.ssb` byte stream. pub fn read_module(r: &mut R) -> eyre::Result { // Header. let magic = read_u32(r)?; @@ -192,10 +551,51 @@ pub fn read_module(r: &mut R) -> eyre::Result { strings.push(String::from_utf8(bytes)?); } + let function_count = read_u32(r)?; + let mut functions = Vec::new(); + for _ in 0..function_count { + let name = read_u32(r)?; + let local_count = read_u32(r)?; + let start_address = read_u32(r)?; + let end_address = read_u32(r)?; + let file = read_u32(r)?; + let parameter_count = read_u32(r)?; + let mut params = Vec::new(); + for _ in 0..parameter_count { + params.push(Parameter { + name: read_u32(r)?, + ty: Type::from_bytes(r)?, + }); + } + let return_count = read_u32(r)?; + let mut ret = Vec::new(); + for _ in 0..return_count { + ret.push(Type::from_bytes(r)?); + } + functions.push(FunctionInfo { + name, + signature: Signature { params, ret }, + local_count, + start_address, + end_address, + file, + }); + } + let label_count = read_u32(r)?; + let mut labels = Vec::new(); + for _ in 0..label_count { + labels.push(LabelInfo { + address: read_u32(r)?, + name: read_u32(r)?, + }); + } + let main = read_u32(r)?; + let main_function = (main != u32::MAX).then_some(main); + let code_count = read_u32(r)?; let mut code = Vec::new(); for _ in 0..code_count { - code.push(PPVMInstruction::from_bytes(r)?); + code.push(decode_instruction(BytecodeInstruction::from_bytes(r)?)); } Ok(PPVMModule { @@ -208,6 +608,9 @@ pub fn read_module(r: &mut R) -> eyre::Result { max_pauli_weight, }, strings, + functions, + labels, + main_function, code, ..Default::default() }) @@ -263,6 +666,11 @@ fn read_u8(r: &mut R) -> eyre::Result { Ok(b[0]) } +fn write_u32(w: &mut W, value: u32) -> eyre::Result<()> { + w.write_all(&value.to_le_bytes())?; + Ok(()) +} + fn read_u16(r: &mut R) -> eyre::Result { let mut b = [0u8; 2]; r.read_exact(&mut b)?; @@ -297,7 +705,7 @@ fn skip_bytes(r: &mut R, n: u64) -> eyre::Result<()> { #[cfg(test)] mod tests { - use vihaco::Value; + use vihaco::{Type, Value}; use super::*; @@ -352,12 +760,12 @@ mod tests { #[test] fn round_trips_code() { use vihaco_circuit_isa::CircuitInstruction; - use vihaco_cpu::Instruction as Cpu; + use vihaco_cpu::RuntimeInstruction as Cpu; let mut m = empty_module(); m.extra.n_qubits = 2; m.code = vec![ - PPVMInstruction::Cpu(Cpu::Const(Value::U64(0))), + PPVMInstruction::Cpu(Cpu::Const(Type::U64, Value::U64(0))), PPVMInstruction::Circuit(CircuitInstruction::H), PPVMInstruction::Circuit(CircuitInstruction::R), PPVMInstruction::Cpu(Cpu::Branch(1)), @@ -378,7 +786,9 @@ mod tests { let mut m = empty_module(); m.extra.n_qubits = 3; m.strings = vec!["hi".to_string()]; - m.code = vec![PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0))]; + m.code = vec![PPVMInstruction::Cpu( + vihaco_cpu::RuntimeInstruction::Return(0), + )]; let mut buf = Vec::new(); write_module(&m, &mut buf).unwrap(); @@ -400,12 +810,12 @@ mod tests { fn compile_to_bytes_round_trips_through_resolve() { let src = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - ret\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.ret 0\n\ }\n"; let bytes = compile_to_bytes(src).unwrap(); @@ -419,11 +829,11 @@ mod tests { fn loaded_bytecode_executes_like_text() { let src = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n circuit.h\n\ - const.u64 0\n const.u64 1\n circuit.cnot\n\ - const.u64 0\n circuit.measure\n\ - const.u64 1\n circuit.measure\n\ - ret\n }\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 bytes = compile_to_bytes(src).unwrap(); let mut machine = crate::composite::PPVM::default(); @@ -436,7 +846,7 @@ mod tests { #[test] fn load_bytecode_file_reads_from_disk() { let src = "device circuit.n_qubits 1;\n\ - fn @main() { const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; let bytes = compile_to_bytes(src).unwrap(); let path = std::env::temp_dir().join("ppvm_load_bytecode_file_test.ssb"); std::fs::write(&path, &bytes).unwrap(); @@ -453,7 +863,9 @@ mod tests { fn read_rejects_truncated_input() { let mut m = empty_module(); m.extra.n_qubits = 2; - m.code = vec![PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0))]; + m.code = vec![PPVMInstruction::Cpu( + vihaco_cpu::RuntimeInstruction::Return(0), + )]; let mut buf = Vec::new(); write_module(&m, &mut buf).unwrap(); @@ -463,7 +875,7 @@ mod tests { } #[test] - fn write_rejects_populated_functions_table() { + fn round_trips_populated_functions_table() { use vihaco::module::{FunctionInfo, Signature}; let mut m = empty_module(); @@ -481,8 +893,8 @@ mod tests { }); let mut buf = Vec::new(); - let err = write_module(&m, &mut buf).unwrap_err(); - assert!(err.to_string().contains("functions"), "err: {err}"); + write_module(&m, &mut buf).unwrap(); + assert_eq!(read_module(&mut buf.as_slice()).unwrap(), m); } #[test] diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index c0f1efa92..569091e15 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, component, observe}; +use vihaco::{Effects, dispatch, observe}; use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; /// Largest qubit count any backend can simulate. The widest size bucket is @@ -71,7 +71,7 @@ pub struct CircuitExecutor, I: TableauIndex, C: SparseVec pub tab: GeneralizedTableau, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl CircuitExecutor where T: Config, @@ -449,7 +449,7 @@ pub struct PauliSumExecutor> { initial: PauliSum, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl PauliSumExecutor where T: Config, @@ -500,7 +500,7 @@ pub struct LossyPauliSumExecutor> { initial: PauliSum, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl LossyPauliSumExecutor where T: Config, @@ -855,7 +855,12 @@ pub enum Circuit { LossyPauliSum(LossyPauliSumCircuit), } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +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. diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 72877bdb5..bcdde6009 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -5,8 +5,8 @@ use eyre::{Result, eyre}; use vihaco::frame::Frame; use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; -use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; -use vihaco::{Effects, Observe, ProgramLoader, Value, composite, observe}; +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 @@ -27,13 +27,13 @@ use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; // existing `crate::composite::{…}` paths keep resolving. pub use crate::device_info::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo}; -pub type Instruction = PPVMInstruction; +type Instruction = ppvm::runtime::Instruction; +pub type PPVMInstruction = Instruction; #[composite] #[derive(Default)] pub struct PPVM { - #[program] - loader: ProgramLoader, + loader: ProgramImage, #[device(0x00)] cpu: CPU, @@ -48,6 +48,8 @@ pub struct PPVM { trace_record: TraceObserver, } +pub(crate) use self::ppvm as ppvm_module; + #[derive(Debug, Clone)] pub enum PPVMEffect { Step(StepOutcome), @@ -137,8 +139,8 @@ impl PartialEq for PPVMInstruction { } } -impl From for PPVMInstruction { - fn from(value: vihaco_cpu::Instruction) -> Self { +impl From for PPVMInstruction { + fn from(value: vihaco_cpu::RuntimeInstruction) -> Self { Self::Cpu(value) } } @@ -150,9 +152,9 @@ impl From for PPVMInstruction { } impl PPVM { - fn resolve_cpu(&mut self, inst: &vihaco_cpu::Instruction) -> eyre::Result { + fn resolve_cpu(&mut self, inst: &vihaco_cpu::RuntimeInstruction) -> eyre::Result { match inst { - vihaco_cpu::Instruction::IndirectCall => { + 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 { @@ -160,7 +162,7 @@ impl PPVM { start_address: function.start_address, }) } - vihaco_cpu::Instruction::Print => { + vihaco_cpu::RuntimeInstruction::Print => { let value = *self.cpu.stack_top()?; match value { vihaco::Value::String(addr) => { @@ -310,14 +312,19 @@ impl PPVM { pub fn load( &mut self, - module: &vihaco::module::Module, + module: &vihaco::module::LocalModule< + Instruction, + vihaco::Value, + vihaco::Type, + PPVMDeviceInfo, + >, ) -> eyre::Result<()> { self.loader.module = module.clone(); Ok(()) } pub fn step_once(&mut self) -> eyre::Result { - let inst = self.peek_instruction()?.clone(); + let inst = self.loader.peek_instruction()?.clone(); let effects = self.execute_effects(inst)?; self.continue_effects(effects) } @@ -330,7 +337,7 @@ impl PPVM { /// 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.peek_instruction().ok().cloned() + self.loader.peek_instruction().ok().cloned() } /// Append one REPL command's lowered VM ops and run just that block against @@ -358,7 +365,7 @@ impl PPVM { /// `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::Module::< + let mut module = vihaco::module::LocalModule::< PPVMInstruction, Value, vihaco::Type, @@ -390,12 +397,14 @@ impl PPVM { let mut instrs = Vec::with_capacity(qubits.len() + params.len() + 1); for &q in qubits { - instrs.push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + 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::Instruction::Const( + instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::F64, Value::F64(p), ))); } @@ -421,15 +430,16 @@ impl PPVM { let msg = self.resolve_cpu(&cpu_inst)?; self.cpu.set_current_pc(self.loader.pc()); let stdout_effect = match (&cpu_inst, &msg) { - (vihaco_cpu::Instruction::Print, vihaco_cpu::CPUMessage::Print(text)) => { - Some(PPVMEffect::Stdout(StdoutEffect(text.clone()))) - } + ( + 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 breakpoint as well, so the debugger that paused + // 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() { @@ -627,30 +637,37 @@ fn parse_observable_terms(info: &PPVMDeviceInfo) -> Result> { #[cfg(test)] mod tests { - use vihaco::{Type, Value, module::Module}; + use vihaco::{Type, Value, module::LocalModule}; use super::*; #[test] fn test_run_ppvm() -> eyre::Result<()> { - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 2; /* - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h */ - let zero = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))); - let one = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(1))); + 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)); /* - const.u64 0 - circuit.t + cpu::cpu.const u64, 0 + circuit::circuit.t */ module.code.push(zero.clone()); @@ -659,9 +676,9 @@ mod tests { .push(PPVMInstruction::Circuit(CircuitInstruction::T)); /* - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot */ module.code.push(zero.clone()); module.code.push(one.clone()); @@ -721,19 +738,21 @@ mod tests { // // fn @main() { ...5-qubit GHZ + 5 measurements... } - let mut module: Module = Module::default(); + 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. /* - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h */ module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(0), ))); module @@ -742,18 +761,20 @@ mod tests { for i in 0..4u64 { /* - const.u64 i - const.u64 i+1 - circuit.cnot + cpu::cpu.const u64, i + cpu::cpu.const u64, i+1 + circuit::circuit.cnot */ module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(i), ))); module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(i + 1), ))); module @@ -764,12 +785,13 @@ mod tests { // Measure all 5 qubits. for q in 0..5u64 { /* - const.u64 q - circuit.measure + cpu::cpu.const u64, q + circuit::circuit.measure */ module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(q), ))); module @@ -797,7 +819,8 @@ mod tests { // 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: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; let mut machine = PPVM::default(); @@ -806,7 +829,10 @@ mod tests { // First command: X on q0 (|0> -> |1>). let x = [ - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))), + PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, + Value::U64(0), + )), PPVMInstruction::Circuit(CircuitInstruction::X), ]; machine.execute_single_instruction(&x)?; @@ -816,7 +842,10 @@ mod tests { // Second command: measure q0. The X from the first command must persist, // so the outcome is deterministically |1>. let measure = [ - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))), + PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, + Value::U64(0), + )), PPVMInstruction::Circuit(CircuitInstruction::Measure), ]; machine.execute_single_instruction(&measure)?; @@ -836,14 +865,15 @@ mod tests { // 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: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; let mut machine = PPVM::default(); machine.load(&module)?; machine.init()?; - // `circuit.h` with nothing on the stack: `pop_u64` fails. + // `circuit::circuit.h` with nothing on the stack: `pop_u64` fails. let missing_operand = [PPVMInstruction::Circuit(CircuitInstruction::H)]; assert!( machine @@ -855,7 +885,7 @@ mod tests { #[test] fn state_string_renders_a_small_device() -> eyre::Result<()> { - let source = "device circuit.n_qubits 2;\nfn @main() { ret }\n"; + 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()?; @@ -876,7 +906,8 @@ mod tests { // 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: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 8; let mut machine = PPVM::default(); machine.load(&module)?; @@ -950,15 +981,15 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ device circuit.coefficient_threshold 1e-8;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - ret\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); - // const.u64 0 / circuit.h / ret = 3 + // cpu::cpu.const u64, 0 / circuit::circuit.h / ret = 3 assert_eq!(machine.loader.module.code.len(), 3); Ok(()) } @@ -967,16 +998,16 @@ mod tests { fn run_program_executes_bell_circuit() -> eyre::Result<()> { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - const.u64 0\n\ - circuit.measure\n\ - const.u64 1\n\ - circuit.measure\n\ - ret\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)?; @@ -991,16 +1022,16 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - const.u64 0\n\ - circuit.measure\n\ - const.u64 1\n\ - circuit.measure\n\ - ret\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)?; @@ -1019,7 +1050,7 @@ mod tests { #[test] fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { - let source = "fn @main() { ret }\n"; + let source = "fn @main() { cpu::cpu.ret 0 }\n"; let mut machine = PPVM::default(); machine.load_program(source)?; let err = machine.init().unwrap_err(); @@ -1031,13 +1062,14 @@ mod tests { fn run_program_reports_parse_errors() { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - circuit.not_a_real_gate\n\ - ret\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}" ); @@ -1045,25 +1077,25 @@ mod tests { // ─── Breakpoints ────────────────────────────────────────────────────── - /// Bell circuit with a `breakpoint` between the two measurements. + /// Bell circuit with a `cpu::cpu.breakpoint` between the two measurements. const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - const.u64 0\n\ - circuit.measure\n\ - breakpoint\n\ - const.u64 1\n\ - circuit.measure\n\ - ret\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 breakpoint and record + // 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)?; @@ -1077,7 +1109,7 @@ mod tests { machine.load_program(BREAKPOINT_PROGRAM)?; machine.init()?; - // Step until the breakpoint pauses us. + // 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()?; @@ -1085,16 +1117,20 @@ mod tests { break; } } - assert_eq!(outcome, StepOutcome::Breakpoint, "breakpoint should pause"); + 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 breakpoint instruction. + // re-hitting the same cpu::cpu.breakpoint instruction. let next = machine.step_once()?; assert_ne!( next, StepOutcome::Breakpoint, - "must move past the breakpoint" + "must move past the cpu::cpu.breakpoint" ); assert!(machine.current_pc() > pc_at_break, "pc must advance"); @@ -1103,10 +1139,11 @@ mod tests { #[test] fn paulisum_truncate_runs_without_error() -> eyre::Result<()> { - // Smoke test: a `circuit.truncate` reaches the PauliSum executor's + // 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: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; module.extra.observable = Some("Z".to_string()); @@ -1126,7 +1163,8 @@ mod tests { // 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: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; module.extra.observable = Some("Z".to_string()); @@ -1135,7 +1173,8 @@ mod tests { module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::String, Value::String(0), ))); module @@ -1158,7 +1197,8 @@ mod tests { // 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: Module = Module::default(); + 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()); @@ -1166,7 +1206,8 @@ mod tests { module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::String, Value::String(0), ))); module @@ -1187,7 +1228,8 @@ mod tests { #[test] fn paulisum_init_rejects_missing_observable() { // Task 8 requires `device circuit.observable` for PauliSum / Lossy. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; @@ -1202,7 +1244,8 @@ mod tests { #[test] fn paulisum_init_rejects_mismatched_observable_length() { - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 2; module.extra.backend = BackendKind::PauliSum; // Three letters but only two qubits — should error. @@ -1219,10 +1262,11 @@ mod tests { #[test] fn tableau_truncate_is_silent_no_op() -> eyre::Result<()> { - // Task 9: `circuit.truncate` on the default Tableau backend should run + // 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: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; // backend defaults to Tableau; no observable needed. module @@ -1243,14 +1287,14 @@ mod tests { fn apply_circuit_instruction_preserves_pc_and_code_len() -> eyre::Result<()> { use crate::measurements::MeasurementOutcome; - // breakpoint; then measure q0. Step to the breakpoint, inject X, resume. + // 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() { breakpoint\n const.u64 0\n circuit.measure\n ret }\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 breakpoint pauses us. + // Run until the cpu::cpu.breakpoint pauses us. loop { if m.step_once()? == StepOutcome::Breakpoint { break; @@ -1289,7 +1333,7 @@ mod tests { // 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() { breakpoint\n const.u64 0\n circuit.measure\n ret }\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()?; @@ -1324,7 +1368,7 @@ mod tests { #[test] fn apply_circuit_instruction_measurement_does_not_grow_the_stack() -> eyre::Result<()> { - // `circuit.measure` pushes its outcome onto the CPU operand stack for + // `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. @@ -1343,16 +1387,18 @@ mod tests { #[test] fn tableau_trace_emits_expectation_on_zero_state() { - // Task 16: `circuit.trace` on the Tableau backend now computes + // 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: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.strings.push("Z0".to_string()); module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::String, Value::String(0), ))); module @@ -1363,7 +1409,7 @@ mod tests { machine.load(&module).unwrap(); machine.init().unwrap(); machine.step_once().unwrap(); // const.string - machine.step_once().unwrap(); // circuit.trace + machine.step_once().unwrap(); // circuit::circuit.trace let trace = machine.trace_record(); assert_eq!(trace.len(), 1); assert!( @@ -1380,14 +1426,16 @@ mod tests { // 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: Module = Module::default(); + 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::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(0), ))); module @@ -1423,14 +1471,16 @@ mod tests { // Same as `paulisum_reset_restores_seeded_observable`, but through the // LossyPauliSum dispatch path. - let mut module: Module = Module::default(); + 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::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(0), ))); module diff --git a/crates/ppvm-vihaco/src/device_info.rs b/crates/ppvm-vihaco/src/device_info.rs index 77e60ac79..8e675b99d 100644 --- a/crates/ppvm-vihaco/src/device_info.rs +++ b/crates/ppvm-vihaco/src/device_info.rs @@ -10,12 +10,11 @@ pub const PPVM_MAGIC: u32 = 0x5050564D; /// Which execution backend the circuit runs on. Selected via the /// `device circuit.backend` header; defaults to `Tableau` so existing /// programs that don't declare a backend keep working. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, vihaco_parser::Parse)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum BackendKind { #[default] Tableau, PauliSum, - #[token = "lossy_paulisum"] LossyPauliSum, } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 3bad3bdca..2d8f4e5e7 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -15,16 +15,15 @@ mod syntax; /// crate directly. pub use vihaco_circuit_isa::CircuitInstruction; -use chumsky::Parser; use vihaco::syntax::{ParsedModule, Resolve}; -use vihaco::{Type, Value, module::Module}; -use vihaco_parser_core::Parse; +use vihaco::{Type, Value, module::LocalModule}; +use crate::composite::ppvm_module as ppvm; use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; -use crate::syntax::{PPVMHeader, PPVMResolver}; +use crate::syntax::{PPVMHeader, PPVMResolver, parse_functions, parse_headers}; /// A fully resolved PPVM module, ready to load into a [`PPVM`]. -pub type PPVMModule = Module; +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. @@ -52,16 +51,18 @@ pub fn run_program(program: &str) -> eyre::Result { } /// Parse `.sst` source into the unresolved AST. -pub fn parse_program(source: &str) -> eyre::Result> { - ParsedModule::::parser() - .parse(source) - .into_result() - .map_err(|errs| eyre::eyre!("parsing failed: {errs:?}")) +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> { +pub fn compile_program(source: &str) -> eyre::Result { PPVMResolver::new().resolve_module(parse_program(source)?) } @@ -91,7 +92,7 @@ mod tests { #[test] fn dump_program_writes_loadable_bytecode() { let src = "device circuit.n_qubits 1;\n\ - fn @main() { const.u64 0\n circuit.measure\n ret }\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(); diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs index 80945f9e1..a0580b6f7 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -127,11 +127,10 @@ mod tests { use crate::measurements::MeasurementOutcome; /// Measures q0 in |0>: every shot is deterministically `0`. - const DETERMINISTIC: &str = - "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit.measure\n ret }\n"; + 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() { const.u64 0\n circuit.h\n const.u64 0\n circuit.measure\n ret }\n"; + 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() diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index e11865068..3b4bc35c4 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -3,37 +3,51 @@ use std::collections::HashMap; -use chumsky::{Parser, error::Simple, extra}; -use vihaco::{ - Type, Value, - module::Module, - syntax::{BodyItem, RawForm, RawOperand, Resolve}, -}; -use vihaco_circuit_isa::CircuitInstruction; -use vihaco_parser_core::Parse; - +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::Parse)] -#[head = "device "] +#[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 { - #[token = "circuit.n_qubits"] - #[delimiters(open = "", close = "", separator = "")] + #[pattern = "circuit.n_qubits $0"] NumQubits(usize), - - #[token = "circuit.coefficient_threshold"] - #[delimiters(open = "", close = "", separator = "")] + #[pattern = "circuit.coefficient_threshold $0"] CoefficientThreshold(f64), - #[token = "circuit.backend"] - #[delimiters(open = "", close = "", separator = "")] - Backend(BackendKind), - - #[token = "circuit.observable"] - #[delimiters(open = "", close = "", separator = "")] - Observable(#[parse_with = "vihaco_parser_core::ident"] String), - - #[token = "circuit.max_pauli_weight"] - #[delimiters(open = "", close = "", separator = "")] + #[pattern = "circuit.backend $0"] + Backend(BackendKindSyntax), + #[pattern = "circuit.observable $0"] + Observable(Ident), + #[pattern = "circuit.max_pauli_weight $0"] MaxPauliWeight(usize), } @@ -49,676 +63,292 @@ impl PPVMResolver { 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; - } - PPVMHeader::Observable(s) => { - info.observable = Some(s); - } - PPVMHeader::MaxPauliWeight(w) => { - info.max_pauli_weight = Some(w); - } + 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 lower_raw(&mut self, raw: RawForm) -> eyre::Result> { - match raw.mnemonic.as_str() { - "ret" => { - let keep = match raw.operands.as_slice() { - [] => 0u32, - [RawOperand::UInt(n)] => u32::try_from(*n) - .map_err(|_| eyre::eyre!("`ret` keep count {n} does not fit in u32"))?, - other => { - return Err(eyre::eyre!( - "`ret` takes 0 or 1 unsigned int operands, got {other:?}" - )); - } - }; - Ok(vec![vihaco_cpu::Instruction::Return(keep).into()]) - } - "const.str" => { - let lit = match raw.operands.as_slice() { - [RawOperand::StringLit(s)] => s.clone(), - other => { - return Err(eyre::eyre!( - "`const.str` takes one string literal, got {other:?}" - )); - } - }; - let addr = u32::try_from(self.strings.len()).map_err(|_| { - eyre::eyre!("string table overflowed u32 at `const.str` lowering") - })?; - self.strings.push(lit); - Ok(vec![ - vihaco_cpu::Instruction::Const(vihaco::Value::String(addr)).into(), - ]) - } - other => Err(eyre::eyre!( - "PPVMResolver: unhandled raw form `{other}` (operands: {:?})", - raw.operands - )), + fn intern(&mut self, value: &str) -> u32 { + if let Some(index) = self.strings.iter().position(|item| item == value) { + return index as u32; } - } -} - -impl Resolve for PPVMResolver { - type Module = Module; - fn resolve_module( - &mut self, - parsed: vihaco::syntax::ParsedModule, - ) -> eyre::Result { - let mut info = PPVMDeviceInfo::default(); - for header in parsed.headers { - Self::apply_header(&mut info, header)?; + 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, } + } - let mut code: Vec = Vec::new(); - let mut labels: HashMap = HashMap::new(); - let mut branch_patches: Vec<(usize, BranchPatch)> = Vec::new(); - let mut call_patches: Vec<(usize, CallPatch)> = Vec::new(); - for function in parsed.functions { - if labels - .insert(function.name.clone(), code.len() as u32) - .is_some() - { - return Err(eyre::eyre!("duplicate function name `@{}`", function.name)); - } - for item in function.body { - match item { - BodyItem::Direct(inst) => code.push(inst), - BodyItem::Raw(raw) => { - if let Some(name) = raw_as_label(&raw) { - if labels.insert(name.clone(), code.len() as u32).is_some() { - return Err(eyre::eyre!("duplicate label `@{name}`")); - } - continue; - } - if let Some(patch) = raw_as_branch(&raw) { - let idx = code.len(); - code.push(patch.placeholder()); - branch_patches.push((idx, patch)); - continue; - } - if let Some(patch) = raw_as_call(&raw)? { - let idx = code.len(); - code.push(patch.placeholder()); - call_patches.push((idx, patch)); - continue; - } - code.extend(self.lower_raw(raw)?); - } - } + 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))); } - } - for (idx, patch) in branch_patches { - patch.apply(&mut code, idx, &labels)?; - } - for (idx, patch) in call_patches { - patch.apply(&mut code, idx, &labels)?; - } - - let strings = std::mem::take(&mut self.strings); - let module = Module { - code, - strings, - extra: info, - ..Default::default() + SurfaceValue::Bare(BareToken(value)) => value, }; - Ok(module) + 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()?), + }) } -} - -type Err<'src> = extra::Err>; -impl<'src> Parse<'src> for PPVMInstruction { - fn parser() -> impl Parser<'src, &'src str, Self, Err<'src>> { - use chumsky::prelude::*; - - let cpu = ::parser().map(PPVMInstruction::Cpu); - - // Reuse the derived parser for all CircuitInstruction variants, - // gated behind the `circuit.` prefix (covers gates, noise channels, - // measure/reset, trace, and truncate — i.e. everything circuit-side). - let circuit = just("circuit") - .then(just('.')) - .ignore_then(::parser()) - .map(PPVMInstruction::Circuit); - - // Try `circuit.` first so CPU doesn't see "circuit" as an identifier. - choice((circuit, cpu)) + 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, + } } } -// ---- Everything below is 1:1 copy from Acamar with Acamar -> PPVM renaming ---- - -/// A deferred branch whose target(s) couldn't be resolved at lowering time -/// because the label may appear later in the function body. Patched in a -/// second pass once all labels are known. -#[derive(Debug)] -enum BranchPatch { - /// `br @target` — fills the `u32` in `cpu::Instruction::Branch`. - Unconditional(String), - /// `br @t, @f` / `cond_br @t, @f` — fills both `u32`s in - /// `cpu::Instruction::ConditionalBranch`. - Conditional(String, String), -} - -/// `@name:` → `Some("name")`. Body parser already emits `@entry:` as a single -/// raw mnemonic with no operands, so the check is purely on the mnemonic -/// shape. -fn raw_as_label(raw: &RawForm) -> Option { - if !raw.operands.is_empty() { - return None; - } - let m = raw.mnemonic.as_str(); - let stripped = m.strip_prefix('@')?.strip_suffix(':')?; - if stripped.is_empty() { - return None; - } - Some(stripped.to_string()) -} +impl Resolve> for PPVMResolver { + type Module = LocalModule; -/// `br @t` / `br @t, @f` / `cond_br @t, @f`. -fn raw_as_branch(raw: &RawForm) -> Option { - let symbols: Vec<&str> = raw - .operands - .iter() - .map(|op| match op { - RawOperand::Symbol(s) => Some(s.as_str()), - _ => None, - }) - .collect::>>()?; + 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)?; + } - match (raw.mnemonic.as_str(), symbols.as_slice()) { - ("br", [t]) => Some(BranchPatch::Unconditional((*t).to_string())), - ("br", [t, f]) | ("cond_br", [t, f]) => { - Some(BranchPatch::Conditional((*t).to_string(), (*f).to_string())) + 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; } - _ => None, - } -} -impl BranchPatch { - fn placeholder(&self) -> PPVMInstruction { - match self { - BranchPatch::Unconditional(_) => vihaco_cpu::Instruction::Branch(u32::MAX).into(), - BranchPatch::Conditional(_, _) => { - vihaco_cpu::Instruction::ConditionalBranch(u32::MAX, u32::MAX).into() + 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; + } } } - } - fn apply( - self, - code: &mut [PPVMInstruction], - idx: usize, - labels: &HashMap, - ) -> eyre::Result<()> { - let lookup = |name: &str| { - labels - .get(name) - .copied() - .ok_or_else(|| eyre::eyre!("undefined label `@{name}`")) - }; - let resolved = match self { - BranchPatch::Unconditional(t) => vihaco_cpu::Instruction::Branch(lookup(&t)?).into(), - BranchPatch::Conditional(t, f) => { - vihaco_cpu::Instruction::ConditionalBranch(lookup(&t)?, lookup(&f)?).into() + 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); + } } - }; - code[idx] = resolved; - Ok(()) - } -} - -/// `call , @target` — symbolic target resolved in a second pass against -/// the same label table that holds branch targets and function entry points. -#[derive(Debug)] -struct CallPatch { - arity: u32, - target: String, -} - -/// `call , @target` → `Some(CallPatch)`. Returns `Ok(None)` for any -/// other mnemonic so the resolver can fall through to `lower_raw`. -fn raw_as_call(raw: &RawForm) -> eyre::Result> { - if raw.mnemonic != "call" { - return Ok(None); - } - match raw.operands.as_slice() { - [RawOperand::UInt(arity), RawOperand::Symbol(target)] => { - let arity = u32::try_from(*arity) - .map_err(|_| eyre::eyre!("`call` arity {arity} does not fit in u32"))?; - Ok(Some(CallPatch { - arity, - target: target.clone(), - })) + 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, + }); } - other => Err(eyre::eyre!( - "`call` expects `, @`, got operands {other:?}" - )), - } -} - -impl CallPatch { - fn placeholder(&self) -> PPVMInstruction { - vihaco_cpu::Instruction::Call(self.arity, u32::MAX).into() - } - - fn apply( - self, - code: &mut [PPVMInstruction], - idx: usize, - labels: &HashMap, - ) -> eyre::Result<()> { - let target = labels - .get(&self.target) - .copied() - .ok_or_else(|| eyre::eyre!("undefined function `@{}`", self.target))?; - code[idx] = vihaco_cpu::Instruction::Call(self.arity, target).into(); - Ok(()) + module.main_function = functions.get("main").copied(); + module.strings = std::mem::take(&mut self.strings); + Ok(module) } } -#[cfg(test)] -mod tests { - use super::*; - use vihaco::syntax::ParsedModule; - - fn parse_module(source: &str) -> ParsedModule { - ParsedModule::::parser() - .parse(source) - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")) - } - - fn raw(mnemonic: &str, operands: Vec) -> RawForm { - RawForm { - mnemonic: mnemonic.to_string(), - operands, +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)) +} - // ─── Header parsing ─────────────────────────────────────────────────── - - #[test] - fn header_parses_n_qubits() { - let got = ::parser() - .parse("device circuit.n_qubits 5") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::NumQubits(5)); - } - - #[test] - fn header_parses_coefficient_threshold() { - let got = ::parser() - .parse("device circuit.coefficient_threshold 1e-10") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::CoefficientThreshold(1e-10)); - } - - #[test] - fn header_n_qubits_rejects_extra_operand() { - // The variant has exactly one field, so the parser consumes one - // integer. Wrapped in a full module, a second integer must trip the - // module-level parser. - let result = ParsedModule::::parser() - .parse( - "device circuit.n_qubits 5 6;\n\ - fn @main() { ret }\n", - ) - .into_result(); - assert!(result.is_err(), "expected parse error, got {result:?}"); - } - - #[test] - fn header_coefficient_threshold_rejects_extra_operand() { - let result = ParsedModule::::parser() - .parse( - "device circuit.coefficient_threshold 1e-10 0.5;\n\ - fn @main() { ret }\n", - ) - .into_result(); - assert!(result.is_err(), "expected parse error, got {result:?}"); - } - - #[test] - fn header_parses_backend_tableau() { - let got = ::parser() - .parse("device circuit.backend tableau") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Backend(BackendKind::Tableau)); - } - - #[test] - fn header_parses_backend_paulisum() { - let got = ::parser() - .parse("device circuit.backend paulisum") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Backend(BackendKind::PauliSum)); - } - - #[test] - fn header_parses_backend_lossy_paulisum() { - let got = ::parser() - .parse("device circuit.backend lossy_paulisum") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Backend(BackendKind::LossyPauliSum)); - } - - #[test] - fn header_parses_observable_single_pauli_word() { - let got = ::parser() - .parse("device circuit.observable ZZIIII") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Observable("ZZIIII".to_string())); - } - - #[test] - fn header_parses_max_pauli_weight() { - let got = ::parser() - .parse("device circuit.max_pauli_weight 8") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::MaxPauliWeight(8)); - } - - #[test] - fn apply_header_sets_n_qubits() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::NumQubits(7)).unwrap(); - assert_eq!(info.n_qubits, 7); - } - - #[test] - fn apply_header_sets_coefficient_threshold() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::CoefficientThreshold(5e-6)).unwrap(); - assert_eq!(info.coefficient_threshold, 5e-6); - } - - #[test] - fn apply_header_sets_backend() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::Backend(BackendKind::PauliSum)).unwrap(); - assert_eq!(info.backend, BackendKind::PauliSum); - } - - #[test] - fn apply_header_sets_observable() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::Observable("ZZ".to_string())).unwrap(); - assert_eq!(info.observable.as_deref(), Some("ZZ")); - } - - #[test] - fn apply_header_sets_max_pauli_weight() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::MaxPauliWeight(4)).unwrap(); - assert_eq!(info.max_pauli_weight, Some(4)); - } - - #[test] - fn device_info_defaults_match_tableau_no_observable_no_truncation() { - let info = PPVMDeviceInfo::default(); - assert_eq!(info.backend, BackendKind::Tableau); - assert_eq!(info.observable, None); - assert_eq!(info.max_pauli_weight, None); - } - - // ─── PPVMInstruction parser dispatch ────────────────────────────────── - - #[test] - fn ppvm_instruction_parses_cpu_const() { - let got = ::parser() - .parse("const.u64 7") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(7))) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_h() { - let got = ::parser() - .parse("circuit.h") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::H) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_cnot() { - let got = ::parser() - .parse("circuit.cnot") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::CNOT) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_measure() { - let got = ::parser() - .parse("circuit.measure") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::Measure) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_rx() { - let got = ::parser() - .parse("circuit.rx") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::RX) - )); - } - - #[test] - fn ppvm_instruction_rejects_bare_circuit_token_without_circuit_prefix() { - // `h` on its own must not parse as Circuit(H) — only `circuit.h` does. - // Without `circuit `, the CPU parser is tried, which should reject - // `h` (not a CPU mnemonic). - let result = ::parser() - .parse("h") - .into_result(); - assert!(result.is_err(), "expected parse error, got {result:?}"); - } - - // ─── lower_raw ──────────────────────────────────────────────────────── - - #[test] - fn lower_raw_ret_emits_return_zero() { - let mut r = PPVMResolver::new(); - let out = r.lower_raw(raw("ret", vec![])).unwrap(); - assert_eq!(out.len(), 1); - assert!(matches!( - out[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0)) - )); - } - - #[test] - fn lower_raw_ret_with_uint_operand_emits_return_n() { - let mut r = PPVMResolver::new(); - let out = r.lower_raw(raw("ret", vec![RawOperand::UInt(2)])).unwrap(); - assert_eq!(out.len(), 1); - assert!(matches!( - out[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(2)) - )); - } - - #[test] - fn lower_raw_ret_with_non_uint_operand_errors() { - let mut r = PPVMResolver::new(); - let err = r - .lower_raw(raw("ret", vec![RawOperand::Symbol("foo".into())])) - .unwrap_err(); - assert!( - err.to_string().contains("`ret` takes 0 or 1 unsigned int"), - "err: {err}" - ); - } - - #[test] - fn lower_raw_unknown_mnemonic_errors() { - let mut r = PPVMResolver::new(); - let err = r.lower_raw(raw("nope", vec![])).unwrap_err(); - assert!(err.to_string().contains("unhandled raw form"), "err: {err}"); - } - - // ─── End-to-end resolver behaviour ──────────────────────────────────── - - #[test] - fn resolver_populates_device_info_from_headers() { - let parsed = parse_module( - "device circuit.n_qubits 3;\n\ - device circuit.coefficient_threshold 1e-8;\n\ - fn @main() { ret }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert_eq!(m.extra.n_qubits, 3); - assert_eq!(m.extra.coefficient_threshold, 1e-8); - } - - #[test] - fn resolver_populates_paulisum_headers() { - let parsed = parse_module( - "device circuit.n_qubits 4;\n\ - device circuit.backend paulisum;\n\ - device circuit.observable ZZII;\n\ - device circuit.max_pauli_weight 8;\n\ - fn @main() { ret }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert_eq!(m.extra.n_qubits, 4); - assert_eq!(m.extra.backend, BackendKind::PauliSum); - assert_eq!(m.extra.observable.as_deref(), Some("ZZII")); - assert_eq!(m.extra.max_pauli_weight, Some(8)); - } - - #[test] - fn resolver_lowers_simple_bell_body() { - // Smoke test the whole pipeline on a tiny bell-like body. - let parsed = parse_module( - "device circuit.n_qubits 2;\n\ - fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - ret\n\ - }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - // const.u64 0 / circuit.h / const.u64 0 / const.u64 1 / circuit.cnot / ret - assert_eq!(m.code.len(), 6); - assert!(matches!( - m.code[1], - PPVMInstruction::Circuit(CircuitInstruction::H) - )); - assert!(matches!( - m.code[4], - PPVMInstruction::Circuit(CircuitInstruction::CNOT) - )); - assert!(matches!( - m.code[5], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0)) - )); - } - - #[test] - fn resolver_resolves_forward_branch_targets() { - let parsed = parse_module( - "fn @main() {\n\ - @loop:\n\ - br @done\n\ - @done:\n\ - ret\n\ - }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert!(matches!( - m.code[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Branch(1)) - )); - } - - #[test] - fn resolver_resolves_conditional_branch_with_two_targets() { - let parsed = parse_module( - "fn @main() {\n\ - @head:\n\ - br @head, @exit\n\ - @exit:\n\ - ret\n\ - }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert!(matches!( - m.code[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::ConditionalBranch(0, 1)) - )); - } - - #[test] - fn resolver_rejects_undefined_branch_target() { - let parsed = parse_module( - "fn @main() {\n\ - br @missing\n\ - ret\n\ - }\n", - ); - let err = PPVMResolver::new().resolve_module(parsed).unwrap_err(); - assert!( - err.to_string().contains("undefined label `@missing`"), - "err: {err}" - ); - } - - #[test] - fn resolver_rejects_duplicate_label() { - let parsed = parse_module( - "fn @main() {\n\ - @same:\n\ - ret\n\ - @same:\n\ - ret\n\ - }\n", - ); - let err = PPVMResolver::new().resolve_module(parsed).unwrap_err(); - assert!( - err.to_string().contains("duplicate label `@same`"), - "err: {err}" - ); - } +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:?}")) } diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index cdf760418..7b7e3119f 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -1,18 +1,18 @@ device circuit.n_qubits 2; fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index d86de1896..43b8190e5 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -1,29 +1,29 @@ device circuit.n_qubits 2; fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - 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. - const.u32 1 - eq.u32 + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 - cond_br @one, @zero + cpu::cpu.cond_br @one, @zero - @one: - const.u64 1 - circuit.x - br @measure_q1 + cpu::cpu.label @one + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @measure_q1 - @zero: - br @measure_q1 + cpu::cpu.label @zero + cpu::cpu.br @measure_q1 - @measure_q1: - const.u64 1 - circuit.measure - ret + cpu::cpu.label @measure_q1 + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index 501797c4f..f877999a0 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -2,29 +2,29 @@ device circuit.n_qubits 2; fn @main() { // X on q0 -> |1>, measure -> outcome is deterministically 1. - const.u64 0 - circuit.x + cpu::cpu.const u64, 0 + circuit::circuit.x - const.u64 0 - 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. - const.u32 1 - eq.u32 + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 - cond_br @one, @zero + cpu::cpu.cond_br @one, @zero - @one: - const.u64 1 - circuit.x - br @measure_q1 + cpu::cpu.label @one + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @measure_q1 - @zero: - br @measure_q1 + cpu::cpu.label @zero + cpu::cpu.br @measure_q1 - @measure_q1: - const.u64 1 - circuit.measure - ret + cpu::cpu.label @measure_q1 + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index c001a5160..1b549f4de 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -4,16 +4,16 @@ 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. - call 0, @run_circuit - ret + cpu::cpu.call 0, run_circuit + cpu::cpu.ret 0 } fn @run_circuit() { - const.u64 1 - circuit.h + cpu::cpu.const u64, 1 + circuit::circuit.h - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure - ret 1 + cpu::cpu.ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_branch_both.sst b/crates/ppvm-vihaco/tests/function_call_branch_both.sst index 46b538f12..f16e00afe 100644 --- a/crates/ppvm-vihaco/tests/function_call_branch_both.sst +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -12,52 +12,52 @@ device circuit.n_qubits 2; // P(kept ∧ outcome = 0) = 0.25 → q1 stays in |0> → m1 = 0 // → P(m1 = 1) = 0.75. fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.f64 0.5 - circuit.loss + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.5 + circuit::circuit.loss - call 0, @measure_q0 + cpu::cpu.call 0, measure_q0 // Stack: [outcome]. Branch on Lost (outcome == 2) first. - dup - const.u32 2 - eq.u32 - cond_br @lost, @kept + cpu::cpu.dup + cpu::cpu.const u32, 2 + cpu::cpu.eq u32 + cpu::cpu.cond_br @lost, @kept - @lost: + 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. - const.u64 1 - circuit.x - br @final + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @final - @kept: + cpu::cpu.label @kept // Stack: [outcome (0 or 1)]. Compare to 1 to derive a bool. - const.u32 1 - eq.u32 - cond_br @outcome_one, @outcome_zero - - @outcome_one: - const.u64 1 - circuit.x - br @final - - @outcome_zero: - br @final - - @final: - const.u64 1 - circuit.measure - halt + 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() { - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure // Stack: [outcome] - ret 1 + cpu::cpu.ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index 15d995737..98e96f7eb 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -3,37 +3,37 @@ device circuit.n_qubits 2; fn @main() { // Put q0 into |+>. - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h // Measure q1 via a helper that returns the outcome on top of the stack. - call 0, @measure_q1 + cpu::cpu.call 0, measure_q1 // Stack: [outcome]. Compare to 1 to derive a bool for cond_br. - const.u32 1 - eq.u32 - cond_br @one, @zero + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @one, @zero - @one: + cpu::cpu.label @one // outcome was 1: apply X to q0 as a correction. - const.u64 0 - circuit.x - br @done + cpu::cpu.const u64, 0 + circuit::circuit.x + cpu::cpu.br @done - @zero: - br @done + cpu::cpu.label @zero + cpu::cpu.br @done - @done: - ret + cpu::cpu.label @done + cpu::cpu.ret 0 } fn @measure_q1() -> u32 { - const.u64 1 - circuit.h + cpu::cpu.const u64, 1 + circuit::circuit.h - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure // Stack: [outcome] - ret 1 + cpu::cpu.ret 1 } diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index 21f8b619b..dd1717db7 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -1,16 +1,16 @@ device circuit.n_qubits 2; fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - const.f64 0.1 - circuit.rx + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.1 + circuit::circuit.rx - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst index 3f42aef14..2b5f7557e 100644 --- a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst +++ b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst @@ -12,17 +12,17 @@ device circuit.observable ZZ; // 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() { - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - const.f64 0.3 - circuit.loss + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.3 + circuit::circuit.loss - circuit.truncate + circuit::circuit.truncate - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst index e36fe339f..42246add1 100644 --- a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst @@ -4,15 +4,15 @@ device circuit.observable ZZ; fn @main() { // Textbook H(0); CNOT(0,1) — emit reversed for Heisenberg propagation. - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h // Trace against |00>: match Z-or-identity on every qubit. - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst index 53cb37c41..7e2683493 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst @@ -10,18 +10,18 @@ fn @main() { // --CNOT(0,1)--> XII // --H(0)------> ZII // Trace against Z/I-only Paulis picks up the coefficient of ZII = 1.0. - const.u64 1 - const.u64 2 - circuit.cnot + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 2 + circuit::circuit.cnot - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst index 9d5f88991..a6ebe6990 100644 --- a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst +++ b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst @@ -3,7 +3,7 @@ device circuit.backend paulisum; device circuit.observable Z; fn @main() { - const.u64 0 - circuit.measure - ret + cpu::cpu.const u64, 0 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst index 7de4f4f99..40fb69e01 100644 --- a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst @@ -5,7 +5,7 @@ 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). - const.str "[XZ]?*" - circuit.trace - ret + cpu::cpu.const str, "[XZ]?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst index 449d92a79..69bf559eb 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst @@ -7,11 +7,11 @@ fn @main() { // 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. - const.u64 0 - const.f64 0.7 - circuit.ry + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.7 + circuit::circuit.ry - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst index a94ee86a7..7c4f7d5da 100644 --- a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst +++ b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst @@ -7,31 +7,31 @@ 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`. - const.u64 0 - const.u64 1 - const.f64 0.1 - circuit.rxx + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.1 + circuit::circuit.rxx - const.u64 0 - const.u64 1 - const.f64 0.05 - circuit.rzz + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.05 + circuit::circuit.rzz - circuit.truncate + circuit::circuit.truncate - const.u64 0 - const.u64 1 - const.f64 0.1 - circuit.rxx + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.1 + circuit::circuit.rxx - const.u64 0 - const.u64 1 - const.f64 0.05 - circuit.rzz + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.05 + circuit::circuit.rzz - circuit.truncate + circuit::circuit.truncate - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/rotxy.sst b/crates/ppvm-vihaco/tests/rotxy.sst index 99e6c50ed..54acb6255 100644 --- a/crates/ppvm-vihaco/tests/rotxy.sst +++ b/crates/ppvm-vihaco/tests/rotxy.sst @@ -3,13 +3,13 @@ device circuit.n_qubits 1; fn @main() { // R(axis_angle = π/2, θ = π) == RY(π), so |0> is sent to |1>. // Stack order for `circuit.r`: qubit, then axis_angle, then theta. - const.u64 0 - const.f64 1.5707963267948966 - const.f64 3.141592653589793 - circuit.r + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 1.5707963267948966 + cpu::cpu.const f64, 3.141592653589793 + circuit::circuit.r - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index a66d9445d..63f377e9e 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -46,7 +46,7 @@ fn hello_circuit_sst_parses_and_runs() { #[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.r` path end to + // 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:?}")); @@ -108,7 +108,7 @@ fn function_call_returns() { #[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 cond_br must therefore take the @one path, + // 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:?}")); @@ -203,7 +203,7 @@ fn function_call_branch_on_both_returned_values() { #[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.cnot; circuit.h`. + // 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. @@ -238,7 +238,7 @@ fn paulisum_multi_term_observable_trace_through_sst() { #[test] fn paulisum_trotter_matches_pure_rust_reference() { // Two Trotter layers of RXX(0.1) + RZZ(0.05), interleaved with explicit - // `circuit.truncate`. The .sst-driven path should agree bit-for-bit with a + // `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. diff --git a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst index d188046f3..795928243 100644 --- a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst @@ -2,17 +2,17 @@ device circuit.n_qubits 2; fn @main() { // Forward Bell prep: H(0); CNOT(0, 1) → |Φ+⟩. - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - 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. - const.str "Z0Z1" - circuit.trace - ret + cpu::cpu.const str, "Z0Z1" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst index 5dd1c6054..a2edcb1dd 100644 --- a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst @@ -2,19 +2,19 @@ device circuit.n_qubits 3; fn @main() { // Forward GHZ prep: H(0); CNOT(0, 1); CNOT(1, 2) → (|000⟩+|111⟩)/√2. - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 1 - const.u64 2 - 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. - const.str "X{3}" - circuit.trace - ret + cpu::cpu.const str, "X{3}" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst index b50d74cdf..913543115 100644 --- a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst @@ -3,11 +3,11 @@ device circuit.n_qubits 1; fn @main() { // RY(θ)|0⟩ = cos(θ/2)|0⟩ + sin(θ/2)|1⟩, so ⟨ψ|Z|ψ⟩ = cos(θ). // Hard-coded θ = 0.7 → cos(0.7) ≈ 0.7648421872844885. - const.u64 0 - const.f64 0.7 - circuit.ry + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.7 + circuit::circuit.ry - const.str "Z{1}" - circuit.trace - ret + cpu::cpu.const str, "Z{1}" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index 6c5e01887..a89fa5a8a 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -7,9 +7,9 @@ edition = "2024" chumsky = "0.10" eyre = "0.6.12" smallvec = "1.15.1" -vihaco = "0.1.1" -vihaco-parser = "0.1.1" -vihaco-parser-core = "0.1.1" +vihaco = "0.4.0" +vihaco-parser = "0.4.0" +vihaco-parser-derive = "0.4.0" [package.metadata.cargo-machete] ignored = ["eyre"] # transitive dependency in vihaco, somehow not handled correctly by machete diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index 71f196e3a..7b265876d 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -2,85 +2,56 @@ // SPDX-License-Identifier: Apache-2.0 use smallvec::SmallVec; -use vihaco::Instruction; use vihaco::Message; -use vihaco_parser::Parse; - -/// The parse trait wired up by `#[derive(Parse)]`, re-exported so downstream -/// crates can call [`CircuitInstruction::parser`] without depending on -/// `vihaco-parser-core` directly to bring the trait into scope. -pub use vihaco_parser_core::Parse as ParseInstruction; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Instruction, Parse)] -pub enum CircuitInstruction { - // NOTE: longer tokens need to go first - TwoQubitPauliError, // needs to go before T - Truncate, // needs to go before T - Trace, // needs to go before T - - // Single-Qubit Clifford gates - X, - Y, - Z, - H, - - #[token = "sqrt_x_adj"] - SqrtXAdj, - - #[token = "sqrt_x"] - SqrtX, - - #[token = "sqrt_y_adj"] - SqrtYAdj, - - #[token = "sqrt_y"] - SqrtY, - - #[token = "s_adj"] - SAdj, - S, - - // Controlled gates - CNOT, - CZ, - - // T gate - TAdj, - T, - - // Two-qubit rotations - RXX, - RYY, - RZZ, - - // Single-qubit rotations - RX, - RY, - RZ, - - // U3 - U3, - - // Measurement & Reset - Measure, - Reset, - - // RXY - R, - - // Loss - Loss, - CorrelatedLoss, - - // Noise - PauliError, - Depolarize2, - Depolarize, + +vihaco::component! { + #[derive(Debug, Default)] + pub component Circuit {} + + instruction { + TwoQubitPauliError, + Truncate, + Trace, + X, + Y, + Z, + H, + SqrtXAdj, + SqrtX, + SqrtYAdj, + SqrtY, + SAdj, + S, + CNOT, + CZ, + TAdj, + T, + RXX, + RYY, + RZZ, + RX, + RY, + RZ, + U3, + Measure, + Reset, + R, + Loss, + CorrelatedLoss, + PauliError, + Depolarize2, + Depolarize, + } } -impl std::fmt::Display for CircuitInstruction { +pub use circuit::{runtime, syntax}; +pub use runtime::Instruction as CircuitInstruction; + +pub type CircuitSurfaceInstruction = syntax::Instruction; + +impl std::fmt::Display for runtime::Instruction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use CircuitInstruction::*; + use runtime::Instruction::*; match self { TwoQubitPauliError => write!(f, "TwoQubitPauliError"), Truncate => write!(f, "Truncate"), @@ -167,52 +138,15 @@ pub struct CircuitEffect { #[cfg(test)] mod tests { - use super::CircuitInstruction::*; + use super::syntax::Instruction::*; use super::*; use chumsky::Parser as _; - use vihaco::instruction::{FromBytes, OpCode, WriteBytes}; - /// Every variant, in declaration order. Anything iterating over the full - /// instruction set (round-trips, opcode uniqueness) goes through this so a - /// newly added variant is automatically covered. - const ALL: &[CircuitInstruction] = &[ - TwoQubitPauliError, - Truncate, - Trace, - X, - Y, - Z, - H, - SqrtXAdj, - SqrtX, - SqrtYAdj, - SqrtY, - SAdj, - S, - CNOT, - CZ, - TAdj, - T, - RXX, - RYY, - RZZ, - RX, - RY, - RZ, - U3, - Measure, - Reset, - R, - Loss, - CorrelatedLoss, - PauliError, - Depolarize2, - Depolarize, - ]; - - fn parse(src: &str) -> CircuitInstruction { - CircuitInstruction::parser() - .parse(src) + 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:?}")) } @@ -258,86 +192,29 @@ mod tests { 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); + assert_eq!(parse("sadj"), SAdj); + assert_eq!(parse("sqrtx"), SqrtX); + assert_eq!(parse("sqrtxadj"), SqrtXAdj); + assert_eq!(parse("sqrty"), SqrtY); + assert_eq!(parse("sqrtyadj"), SqrtYAdj); } #[test] fn rejects_unknown_token() { - assert!(CircuitInstruction::parser().parse("nope").has_errors()); + assert!( + CircuitSurfaceInstruction::parser() + .parse("circuit.nope") + .has_errors() + ); } #[test] fn rejects_pascal_case_token() { // The parse token is lowercase; the Display form must not parse back. - assert!(CircuitInstruction::parser().parse("CNOT").has_errors()); - } - - // ─── Display: PascalCase variant names ──────────────────────────────── - - #[test] - fn display_uses_pascal_case_names() { - assert_eq!(H.to_string(), "H"); - assert_eq!(CNOT.to_string(), "CNOT"); - assert_eq!(TwoQubitPauliError.to_string(), "TwoQubitPauliError"); - assert_eq!(Trace.to_string(), "Trace"); - assert_eq!(Truncate.to_string(), "Truncate"); - // Custom-token variants display their Rust name, not the parse token. - assert_eq!(SqrtXAdj.to_string(), "SqrtXAdj"); - assert_eq!(SAdj.to_string(), "SAdj"); - } - - // ─── Instruction codec (derived OpCode / WriteBytes / FromBytes) ────── - - #[test] - fn opcodes_are_unit_width() { - // All variants are field-less, so each encodes to a single byte. - assert_eq!(CircuitInstruction::width(), 1); - } - - #[test] - fn opcodes_are_unique() { - let mut seen = std::collections::HashSet::new(); - for inst in ALL { - assert!( - seen.insert(inst.opcode()), - "duplicate opcode {} for {inst:?}", - inst.opcode() - ); - } - assert_eq!(seen.len(), ALL.len()); - } - - #[test] - fn opcodes_match_declaration_order() { - // Opcodes default to the variant index, which is the on-disk contract - // for bytecode. Reordering variants silently breaks old bytecode, so - // pin the assignment here. - for (index, inst) in ALL.iter().enumerate() { - assert_eq!(inst.opcode() as usize, index, "{inst:?}"); - } - } - - #[test] - fn write_then_read_round_trips_every_variant() { - for inst in ALL { - let mut buf = Vec::new(); - inst.write_bytes(&mut buf).unwrap(); - assert_eq!(buf, [inst.opcode()], "{inst:?} should encode to one byte"); - - let mut cursor = std::io::Cursor::new(buf); - let back = CircuitInstruction::from_bytes(&mut cursor).unwrap(); - assert_eq!(back, *inst); - } - } - - #[test] - fn from_bytes_rejects_unknown_opcode() { - let mut cursor = std::io::Cursor::new([0xFFu8]); - let err = CircuitInstruction::from_bytes(&mut cursor).unwrap_err(); - assert!(err.to_string().contains("invalid opcode"), "err: {err}"); + assert!( + CircuitSurfaceInstruction::parser() + .parse("circuit.CNOT") + .has_errors() + ); } } diff --git a/docs/changelog/vihaco-0.4.md b/docs/changelog/vihaco-0.4.md new file mode 100644 index 000000000..e94fd5ecc --- /dev/null +++ b/docs/changelog/vihaco-0.4.md @@ -0,0 +1,369 @@ +# PPVM vihaco 0.4.0 changes + +This changelog describes the completed migration of `ppvm-vihaco` and its +supporting ISA crate from vihaco 0.1.1 to vihaco 0.4.0. It is an informative +reference for users and contributors who need to understand what changed, +especially in `.sst` instruction syntax. + +PPVM now uses the vihaco 0.4 model: + +```text +source instruction -> surface instruction -> runtime instruction +``` + +The lowering step resolves names, types, literals, and addresses while +preserving one source instruction per runtime instruction. Syntax sugar that +expands one source instruction into several runtime instructions is not part +of the language. + +## Migration summary + +The migration is implemented across the PPVM runtime, circuit ISA, parser, +bytecode codec, CLI/TUI call sites, and `.sst` fixtures. The implementation +uses v0.4 surface and runtime instruction types, generated component +instruction sets, explicit one-to-one lowering, and bytecode v2 metadata for +functions, labels, signatures, and the entry point. + +The full workspace test suite is the semantic verification gate. It covers +source execution, bytecode round trips, function calls, branching, +measurements, all PPVM backends, traces, loss, truncation, and source/bytecode +parity. + +The implementation was guided by Stellarscope PR #75, commit `b9e24258`. + +## Semantic preservation + +This was an API and syntax migration, not a simulator-behavior migration. +The implementation preserves PPVM semantics. In particular, it preserves: + +- Heisenberg/Pauli propagation direction or gate ordering; +- operand stack ordering, including the order in which qubit addresses and + floating-point parameters are popped; +- measurement and reset behavior; +- tableau, PauliSum, and LossyPauliSum backend results; +- truncation thresholds, maximum Pauli weights, and loss-channel behavior; +- branch and conditional-branch behavior after label resolution; +- function-call behavior, return-value counts, and function boundaries; +- string-table values used by `trace`, `print`, and other string-consuming + instructions; +- seeded randomness and shot-level reproducibility; +- bytecode round-trip behavior and equivalence between source and bytecode + execution. + +Source/runtime instruction conversion was verified in two ways: + +1. **Implementation review:** the surface-to-runtime lowering preserves + operand values, operand order, instruction order, control-flow targets, + and backend dispatch. Any source-only construct, such as a label, is + explicitly identified as metadata rather than silently dropped. +2. **Existing behavioral tests:** the tests already in this repository remain + the semantic oracle. Their source spellings and expected enum names were + migrated without weakening or replacing their behavioral assertions. + +The completed implementation passes the existing tests for +GHZ/Bell circuits, measurements, branching, function calls, traces, +PauliSum/LossyPauliSum behavior, truncation, reset, bytecode serialization, +and source/bytecode parity. Parser tests additionally assert the one-to-one +source-to-runtime mapping, while the existing execution tests assert that the +mapping preserves behavior. + +## Dependency changes + +### Workspace and PPVM dependencies + +PPVM now uses the published 0.4.0 crates consistently: + +- `vihaco = "0.4.0"` +- `vihaco-cpu = "0.4.0"` +- `vihaco-parser = "0.4.0"` +- `vihaco-parser-derive = "0.4.0"` + +The migration removed direct dependencies on: + +- `vihaco-parser-core` +- `vihaco-derive` +- the old 0.1.1 vihaco crates + +`vihaco-circuit-isa` was migrated as well. The workspace now has one vihaco +version, and its instruction type is usable as a v0.4 component instruction +set. + +### Parser imports and derives + +Old parser-core imports were replaced: + +```rust +use vihaco_parser_core::Parse; +``` + +with: + +```rust +use vihaco_parser::Parse; +``` + +`vihaco_parser_derive::Parse` is used for derives required by the 0.4 API. +The 0.4 derive uses `#[pattern = ...]` and syntax-class metadata; the old +`#[head]`, `#[token]`, and `#[delimiters]` form is no longer the general +instruction-definition mechanism. + +## Component and instruction-set changes + +### Circuit component + +The circuit instruction set is defined with `vihaco::component!`. Its generated +module exposes separate surface and runtime types: + +```rust +vihaco::component! { + pub component Circuit { + // state fields + } + + instruction { + X, + H, + // ... + } +} + +pub use circuit::{runtime, syntax}; +``` + +The generated runtime instruction type is used by `#[dispatch]` and by the +composite. The generated syntax instruction type is used by the parser. This +implements `HasInstructionSet`, which the v0.4 `#[composite]` macro requires. + +The hand-written `vihaco_circuit_isa::CircuitInstruction` was replaced by the +generated v0.4 instruction type. `CircuitMessage` and `CircuitEffect` remain +application-level payload types. + +### CPU instruction types + +vihaco 0.4 distinguishes: + +- `vihaco_cpu::SurfaceInstruction`: parsed source instructions; +- `vihaco_cpu::RuntimeInstruction`: executable instructions; +- `vihaco_cpu::SurfaceType` and `SurfaceValue`: source-level typed operands. + +PPVM parses into surface instructions and lowers them to +`RuntimeInstruction`, resolving labels, function names, types, and +string-table indices along the way. + +### Composite instruction type + +The composite-generated instruction enum is the runtime instruction type +generated by the composite, for example: + +```rust +pub type PPVMInstruction = ppvm::runtime::Instruction; +``` + +Conversions now exist from the runtime CPU and runtime circuit instruction +types into that enum. The loader uses the v0.4 `ProgramImage` and +`LocalModule`, replacing the removed `Module`/old loader API combination. + +## Parser and resolver changes + +The old `ParsedModule`/`BodyItem`/`RawForm` pipeline was replaced by the v0.4 +pipeline: + +```rust +ParsedModule> +``` + +and: + +```rust +impl Resolve for PPVMResolver { + type Module = LocalModule; +} +``` + +The resolver now performs these operations: + +1. Applies device headers to `PPVMDeviceInfo`. +2. Assigns function IDs and addresses. +3. Records source labels as metadata and resolves branch/call targets. +4. Converts CPU surface instructions to runtime instructions. +5. Converts circuit surface instructions to circuit runtime instructions. +6. Interns string constants into the module string table. +7. Populates functions, labels, `main_function`, code, strings, and device info. + +There is no generic fallback for arbitrary `RawForm` values. Unknown +instructions and malformed operands fail during surface parsing. + +## Instruction syntax changes + +The v0.4 composite syntax qualifies an instruction with both the composite +field and the component dialect: + +```text +::. +``` + +For PPVM this means CPU instructions use `cpu::cpu.*`, and circuit +instructions use `circuit::circuit.*`. + +### CPU instructions + +| Old PPVM spelling | v0.4 spelling | Runtime result | +| --- | --- | --- | +| `const.u64 0` | `cpu::cpu.const u64, 0` | `RuntimeInstruction::Const(Type::U64, Value::U64(0))` | +| `const.u32 1` | `cpu::cpu.const u32, 1` | `RuntimeInstruction::Const(Type::U32, Value::U32(1))` | +| `const.i64 -1` | `cpu::cpu.const i64, -1` | typed `Const` | +| `const.f64 0.5` | `cpu::cpu.const f64, 0.5` | typed `Const` | +| `const.bool true` | `cpu::cpu.const bool, true` | typed `Const` | +| `const.str "Z?*"` | `cpu::cpu.const str, "Z?*"` | one typed `Const`, with string interning | +| `ret` | `cpu::cpu.ret 0` | `RuntimeInstruction::Return(0)` | +| `ret 1` | `cpu::cpu.ret 1` | `RuntimeInstruction::Return(1)` | +| `br @done` | `cpu::cpu.br @done` | one patched `Branch` | +| `cond_br @yes, @no` | `cpu::cpu.cond_br @yes, @no` | one patched `ConditionalBranch` | +| `call 0, @measure` | `cpu::cpu.call 0, measure` | one patched `Call` | +| `@label:` | `cpu::cpu.label @label` | source label metadata; no runtime instruction | +| `breakpoint` | `cpu::cpu.breakpoint` | one `Breakpoint` | +| `halt` | `cpu::cpu.halt` | one `Halt` | +| `print` | `cpu::cpu.print` | one `Print` | +| `heap_alloc 4` | `cpu::cpu.heap_alloc 4` | one `HeapAlloc` | +| `load ...` | `cpu::cpu.load ...` | one typed `Load` | +| `store ...` | `cpu::cpu.store ...` | one typed `Store` | +| arithmetic/logical CPU op | `cpu::cpu. ...` | one runtime CPU instruction | + +The exact operand spelling for typed CPU instructions follows the v0.4 CPU +surface grammar. In particular, types and values are separate source +operands: `const f64, 0.5`, not `const.f64 0.5`. + +`const.str` is not a multi-instruction expansion. It is a source-level typed +constant whose string value is interned while producing one runtime `Const`. +The old spelling was removed because it encoded the type in the mnemonic. + +### Circuit instructions + +Circuit instructions do not take qubit or numeric operands in their mnemonic. +PPVM pushes operands with CPU constants, then executes one circuit instruction +that consumes the required values from the CPU stack. + +| Old PPVM spelling | v0.4 spelling | Values consumed from stack | +| --- | --- | --- | +| `circuit.x` | `circuit::circuit.x` | qubit | +| `circuit.y` | `circuit::circuit.y` | qubit | +| `circuit.z` | `circuit::circuit.z` | qubit | +| `circuit.h` | `circuit::circuit.h` | qubit | +| `circuit.s` / `circuit.s_adj` | `circuit::circuit.s` / `circuit::circuit.s_adj` | qubit | +| `circuit.sqrt_x` / `circuit.sqrt_x_adj` | `circuit::circuit.sqrt_x` / `circuit::circuit.sqrt_x_adj` | qubit | +| `circuit.sqrt_y` / `circuit.sqrt_y_adj` | `circuit::circuit.sqrt_y` / `circuit::circuit.sqrt_y_adj` | qubit | +| `circuit.t` / `circuit.t_adj` | `circuit::circuit.t` / `circuit::circuit.t_adj` | qubit | +| `circuit.cnot` | `circuit::circuit.cnot` | two qubits | +| `circuit.cz` | `circuit::circuit.cz` | two qubits | +| `circuit.rx`, `ry`, `rz` | `circuit::circuit.rx`, `ry`, `rz` | qubit, float | +| `circuit.rxx`, `ryy`, `rzz` | `circuit::circuit.rxx`, `ryy`, `rzz` | two qubits, float | +| `circuit.r` | `circuit::circuit.r` | qubit, two floats | +| `circuit.u3` | `circuit::circuit.u3` | qubit, three floats | +| `circuit.measure` | `circuit::circuit.measure` | qubit | +| `circuit.reset` | `circuit::circuit.reset` | qubit | +| `circuit.depolarize` | `circuit::circuit.depolarize` | qubit, float | +| `circuit.depolarize2` | `circuit::circuit.depolarize2` | two qubits, float | +| `circuit.paulierror` | `circuit::circuit.pauli_error` | qubit, three probabilities | +| `circuit.two_qubit_pauli_error` | `circuit::circuit.two_qubit_pauli_error` | two qubits, fifteen probabilities | +| `circuit.loss` | `circuit::circuit.loss` | qubit, float | +| `circuit.correlated_loss` | `circuit::circuit.correlated_loss` | two qubits, three probabilities | +| `circuit.trace` | `circuit::circuit.trace` | pattern string | +| `circuit.truncate` | `circuit::circuit.truncate` | none | + +The generated component defines the names explicitly where needed. Each +listed source instruction maps to exactly one generated circuit runtime +instruction. + +### Device headers + +The PPVM device headers remain conceptually the same and are now parsed with +the v0.4 header syntax: + +```text +device circuit.n_qubits 2; +device circuit.backend paulisum; +device circuit.observable ZZ; +device circuit.coefficient_threshold 1e-10; +device circuit.max_pauli_weight 8; +``` + +They are metadata, not runtime instructions, and their meaning is unchanged. + +## One-to-one lowering behavior + +The implementation follows these rules: + +- A typed `const` produces one runtime `Const`. +- `ret`, branch, conditional branch, and call each produce one runtime CPU + instruction. +- A circuit mnemonic produces one runtime circuit instruction. +- A source label records a label address but produces no executable runtime + instruction. Labels are metadata, not instructions. +- Symbolic branch and call targets may be resolved in a separate pass, but + resolution replaces operands on the same instruction rather than emitting + additional instructions. +- No `play`, `poly`, `digi`, combined gate, or measurement shorthand expands + into a constant plus another instruction. +- Stack setup is written explicitly in the source. For example, a + measurement requires an explicit `cpu::cpu.const u64, 0` followed by + `circuit::circuit.measure`. + +## Bytecode and runtime changes + +Bytecode serialization and tests were updated for v0.4 runtime instructions: + +- the codec uses `RuntimeInstruction`, not the removed + `vihaco_cpu::Instruction`; +- typed `Const` instructions include their `Type` argument; +- modules use `LocalModule` and the v0.4 module fields; +- the existing PPVM string table and device-info serialization is preserved; +- labels and function metadata are serialized so source and bytecode execution have + the same resolved control-flow and call behavior. PPVM bytecode v2 stores + function signatures, function ranges, labels, and `main_function`; +- round-trip tests cover every runtime instruction family used by PPVM. + +## Call-site and fixture changes + +All embedded programs and `.sst` fixtures were updated, including: + +- `crates/ppvm-vihaco/tests/*.sst`; +- `crates/ppvm-vihaco/src` test programs; +- `crates/ppvm-cli/examples/*.sst`; +- CLI and TUI breakpoint/trace programs; +- README and documentation examples. + +For every fixture, verification covers both: + +1. the new source parses into the intended surface instruction; and +2. resolution emits exactly one corresponding runtime instruction per source + instruction, excluding source-only labels. + +The fixtures run through the existing execution assertions, which compare +measurements, traces, states, branch outcomes, and bytecode results with the +pre-migration behavior. A fixture that parses but changes a result fails the +migration's semantic-preservation requirement. + +## Verification + +The completed migration was verified with: + +```bash +cargo fmt --all -- --check +cargo check -p ppvm-vihaco +cargo test -p vihaco-circuit-isa +cargo test -p ppvm-vihaco +cargo test -p ppvm-cli +cargo test --workspace +``` + +The workspace test suite passes, including PPVM source/bytecode fixtures, +CLI/TUI tests, circuit ISA tests, and the existing simulator behavior tests. + +The dependency tree was also checked to contain only the v0.4 vihaco family: + +```bash +cargo tree -i vihaco@0.1.1 +``` + +That command reports no PPVM-related dependency path for the migrated +`vihaco-circuit-isa`.